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 fa784ee6..2f5b3786 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
@@ -58,6 +58,7 @@ class CopySelectedQueryResultsFeatureTest(BaseFeatureTest):
self._copies_rectangular_selection()
self._shift_resizes_rectangular_selection()
self._shift_resizes_column_selection()
+ self._mouseup_outside_grid_still_makes_a_selection()
def _copies_rows(self):
pyperclip.copy("old clipboard contents")
@@ -103,11 +104,12 @@ class CopySelectedQueryResultsFeatureTest(BaseFeatureTest):
def _copies_rectangular_selection(self):
pyperclip.copy("old clipboard contents")
- topLeftCell = self.page.find_by_xpath("//div[contains(@class, 'slick-cell') and contains(., 'Some-Other-Name')]")
- bottomRightCell = self.page.find_by_xpath("//div[contains(@class, 'slick-cell') and contains(., '14')]")
-
- ActionChains(self.page.driver).click_and_hold(topLeftCell).move_to_element(bottomRightCell).release(bottomRightCell).perform()
+ top_left_cell = self.page.find_by_xpath(
+ "//div[contains(@class, 'slick-cell') and contains(., 'Some-Other-Name')]")
+ 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(bottom_right_cell) \
+ .release(bottom_right_cell).perform()
ActionChains(self.page.driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()
self.assertEqual("""'Some-Other-Name','22'
@@ -116,9 +118,11 @@ class CopySelectedQueryResultsFeatureTest(BaseFeatureTest):
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)\
+ 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()
@@ -131,7 +135,7 @@ class CopySelectedQueryResultsFeatureTest(BaseFeatureTest):
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)\
+ 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()
@@ -142,6 +146,22 @@ class CopySelectedQueryResultsFeatureTest(BaseFeatureTest):
'Yet-Another-Name','14'""",
pyperclip.paste())
+ def _mouseup_outside_grid_still_makes_a_selection(self):
+ pyperclip.copy("old clipboard contents")
+
+ bottom_right_cell = self.page.find_by_xpath(
+ "//div[contains(@class, 'slick-cell') and contains(., 'cool info')]")
+
+ load_button = self.page.find_by_xpath("//button[@id='btn-load-file']")
+ ActionChains(self.page.driver).click_and_hold(bottom_right_cell) \
+ .move_to_element(load_button) \
+ .release(load_button) \
+ .perform()
+
+ ActionChains(self.page.driver).key_down(Keys.CONTROL).send_keys('c').key_up(Keys.CONTROL).perform()
+
+ self.assertEqual("'cool info'", pyperclip.paste())
+
def after(self):
self.page.close_query_tool()
self.page.remove_server(self.server)
diff --git a/web/pgadmin/static/js/selection/column_selector.js b/web/pgadmin/static/js/selection/column_selector.js
index 8806b68c..53cdad82 100644
--- a/web/pgadmin/static/js/selection/column_selector.js
+++ b/web/pgadmin/static/js/selection/column_selector.js
@@ -8,36 +8,38 @@ define([
var gridEventBus = new Slick.EventHandler();
var init = function (grid) {
- gridEventBus.subscribe(grid.onHeaderClick, function (event, eventArgument) {
- var columnDefinition = eventArgument.column;
+ gridEventBus.subscribe(grid.onHeaderClick, handleHeaderClick.bind(null, grid));
+ grid.getSelectionModel().onSelectedRangesChanged
+ .subscribe(handleSelectedRangesChanged.bind(null, grid));
+ };
- grid.focus();
+ var handleHeaderClick = function (grid, event, args) {
+ var columnDefinition = args.column;
- if (isColumnSelectable(columnDefinition)) {
- var $columnHeader = $(event.target);
- if(hasClickedChildOfColumnHeader(event)) {
- $columnHeader = $(event.target).parents(".slick-header-column");
- }
- $columnHeader.toggleClass('selected');
+ grid.focus();
- updateRanges(grid, columnDefinition.id);
+ if (isColumnSelectable(columnDefinition)) {
+ var $columnHeader = $(event.target);
+ if (hasClickedChildOfColumnHeader(event)) {
+ $columnHeader = $(event.target).parents(".slick-header-column");
}
- });
- grid.getSelectionModel().onSelectedRangesChanged
- .subscribe(handleSelectedRangesChanged.bind(null, grid));
+ $columnHeader.toggleClass('selected');
+
+ updateRanges(grid, columnDefinition.id);
+ }
};
- var handleSelectedRangesChanged = function (grid, event, ranges) {
- $('.slick-header-column.selected')
- .each(function (index, columnHeader) {
- var $spanHeaderColumn = $(columnHeader).find('[data-cell-type="column-header-row"]');
- var columnIndex = grid.getColumnIndex($spanHeaderColumn.data('column-id'));
- var isStillSelected = RangeSelectionHelper.isRangeSelected(ranges,
- RangeSelectionHelper.rangeForColumn(grid, columnIndex));
- if (!isStillSelected) {
- $(columnHeader).toggleClass('selected')
- }
- });
+ var handleSelectedRangesChanged = function (grid, event, selectedRanges) {
+ $('.slick-header-column').each(function (index, columnHeader) {
+ var $spanHeaderColumn = $(columnHeader).find('[data-cell-type="column-header-row"]');
+ var columnIndex = grid.getColumnIndex($spanHeaderColumn.data('column-id'));
+
+ if (isColumnSelected(grid, selectedRanges, columnIndex)) {
+ $(columnHeader).addClass('selected');
+ } else {
+ $(columnHeader).removeClass('selected');
+ }
+ });
};
var updateRanges = function (grid, columnId) {
@@ -68,6 +70,19 @@ define([
return columnDefinition.selectable !== false;
};
+ var isColumnSelected = function (grid, selectedRanges, columnIndex) {
+ var allRangesAreRows = RangeSelectionHelper.areAllRangesCompleteRows(grid, selectedRanges);
+ return isAnyCellSelectedInColumn(grid, selectedRanges, columnIndex) && !allRangesAreRows;
+ };
+
+ var isAnyCellSelectedInColumn = function (grid, selectedRanges, columnIndex) {
+ var isStillSelected = RangeSelectionHelper.isRangeEntirelyWithinSelectedRanges(selectedRanges,
+ RangeSelectionHelper.rangeForColumn(grid, columnIndex));
+ var cellSelectedInColumn = RangeSelectionHelper.isAnyCellOfColumnSelected(selectedRanges, columnIndex);
+
+ return isStillSelected || cellSelectedInColumn;
+ };
+
var getColumnDefinitions = function (columnDefinitions) {
return _.map(columnDefinitions, function (columnDefinition) {
if (isColumnSelectable(columnDefinition)) {
@@ -75,7 +90,10 @@ define([
"" +
- " " + columnDefinition.name + "" +
+ " " +
+ " " + columnDefinition.display_name + "" +
+ " " + columnDefinition.column_type + "" +
+ " " +
"";
return _.extend(columnDefinition, {
name: name
diff --git a/web/pgadmin/static/js/selection/copy_data.js b/web/pgadmin/static/js/selection/copy_data.js
index 166219ba..ad79d3eb 100644
--- a/web/pgadmin/static/js/selection/copy_data.js
+++ b/web/pgadmin/static/js/selection/copy_data.js
@@ -15,7 +15,7 @@ function ($, _, clipboard, RangeSelectionHelper, rangeBoundaryNavigator) {
var data = grid.getData();
var rows = grid.getSelectedRows();
- if (allTheRangesAreFullRows(selectedRanges, columnDefinitions)) {
+ if (RangeSelectionHelper.areAllRangesCompleteRows(grid, selectedRanges)) {
self.copied_rows = rows.map(function (rowIndex) {
return data[rowIndex];
});
diff --git a/web/pgadmin/static/js/selection/range_selection_helper.js b/web/pgadmin/static/js/selection/range_selection_helper.js
index 3d121f4f..936530ff 100644
--- a/web/pgadmin/static/js/selection/range_selection_helper.js
+++ b/web/pgadmin/static/js/selection/range_selection_helper.js
@@ -9,7 +9,26 @@ define(['slickgrid'], function () {
var isRangeSelected = function (selectedRanges, range) {
return _.any(selectedRanges, function (selectedRange) {
return isSameRange(selectedRange, range)
- })
+ });
+ };
+
+ var isAnyCellOfColumnSelected = function (selectedRanges, column) {
+ return _.any(selectedRanges, function (selectedRange) {
+ return selectedRange.fromCell <= column && selectedRange.toCell >= column
+ });
+ };
+
+ var isAnyCellOfRowSelected = function (selectedRanges, row) {
+ return _.any(selectedRanges, function (selectedRange) {
+ return selectedRange.fromRow <= row && selectedRange.toRow >= row
+ });
+ };
+
+ var isRangeEntirelyWithinSelectedRanges = function (selectedRanges, range) {
+ return _.any(selectedRanges, function (selectedRange) {
+ return selectedRange.fromCell <= range.fromCell && selectedRange.toCell >= range.toCell &&
+ selectedRange.fromRow <= range.fromRow && selectedRange.toRow >= range.toRow;
+ });
};
var removeRange = function (selectedRanges, range) {
@@ -30,10 +49,7 @@ define(['slickgrid'], function () {
};
var areAllRangesSingleColumns = function (ranges, grid) {
- return _.every(ranges, function (range) {
- return range.fromCell == range.toCell &&
- range.fromRow == 0 && range.toRow == grid.getDataLength() - 1
- })
+ return _.every(ranges, isRangeAColumn.bind(this, grid))
};
var rangeForRow = function (grid, rowId) {
@@ -62,6 +78,12 @@ define(['slickgrid'], function () {
return !_.isUndefined(columnDefinitions[0].pos);
};
+ var areAllRangesCompleteColumns = function (grid, ranges) {
+ return _.every(ranges, function (range) {
+ return rangeHasCompleteColumns(grid, range);
+ })
+ };
+
var areAllRangesCompleteRows = function (grid, ranges) {
return _.every(ranges, function (range) {
return rangeHasCompleteRows(grid, range);
@@ -78,10 +100,34 @@ define(['slickgrid'], function () {
return indexArray;
};
- function rangeHasCompleteRows(grid, range) {
- var firstDataColumn = isFirstColumnData(grid.getColumns()) ? 0 : 1;
- return range.fromCell == firstDataColumn &&
- range.toCell == grid.getColumns().length - 1;
+ var isRangeAColumn = function (grid, range) {
+ return range.fromCell == range.toCell &&
+ range.fromRow == 0 && range.toRow == grid.getDataLength() - 1;
+ };
+
+ var rangeHasCompleteColumns = function (grid, range) {
+ return range.fromRow === 0 && range.toRow === grid.getDataLength() - 1;
+ };
+
+ var rangeHasCompleteRows = function (grid, range) {
+ return range.fromCell === getFirstDataColumnIndex(grid) &&
+ range.toCell === getLastDataColumnIndex(grid);
+ };
+
+ function getFirstDataColumnIndex(grid) {
+ return _.findIndex(grid.getColumns(), function (columnDefinition) {
+ var pos = columnDefinition.pos;
+
+ return !_.isUndefined(pos) && isSelectable(columnDefinition);
+ });
+ }
+
+ function getLastDataColumnIndex(grid) {
+ return _.findLastIndex(grid.getColumns(), isSelectable);
+ }
+
+ function isSelectable(columnDefinition) {
+ return (_.isUndefined(columnDefinition.selectable) || columnDefinition.selectable === true);
}
function selectAll(grid) {
@@ -98,12 +144,19 @@ define(['slickgrid'], function () {
areAllRangesSingleRows: areAllRangesSingleRows,
areAllRangesSingleColumns: areAllRangesSingleColumns,
areAllRangesCompleteRows: areAllRangesCompleteRows,
+ areAllRangesCompleteColumns: areAllRangesCompleteColumns,
rangeForRow: rangeForRow,
rangeForColumn: rangeForColumn,
isEntireGridSelected: isEntireGridSelected,
getRangeOfWholeGrid: getRangeOfWholeGrid,
isFirstColumnData: isFirstColumnData,
- selectAll: selectAll,
getIndexesOfCompleteRows: getIndexesOfCompleteRows,
+ selectAll: selectAll,
+ isRangeAColumn: isRangeAColumn,
+ rangeHasCompleteColumns: rangeHasCompleteColumns,
+ rangeHasCompleteRows: rangeHasCompleteRows,
+ isAnyCellOfColumnSelected: isAnyCellOfColumnSelected,
+ isRangeEntirelyWithinSelectedRanges: isRangeEntirelyWithinSelectedRanges,
+ isAnyCellOfRowSelected: isAnyCellOfRowSelected,
}
});
\ 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 1088f879..33d5a72e 100644
--- a/web/pgadmin/static/js/selection/row_selector.js
+++ b/web/pgadmin/static/js/selection/row_selector.js
@@ -1,39 +1,44 @@
-define(['jquery', 'sources/selection/range_selection_helper', 'slickgrid'], function ($, RangeSelectionHelper) {
+define([
+ 'jquery',
+ 'sources/selection/range_selection_helper',
+ 'slickgrid'
+], function ($, RangeSelectionHelper) {
var RowSelector = function () {
var Slick = window.Slick;
var gridEventBus = new Slick.EventHandler();
var init = function (grid) {
- grid.getSelectionModel()
- .onSelectedRangesChanged.subscribe(handleSelectedRangesChanged.bind(null, grid));
+ grid.getSelectionModel().onSelectedRangesChanged
+ .subscribe(handleSelectedRangesChanged.bind(null, grid));
gridEventBus
- .subscribe(grid.onClick, handleClick.bind(null, grid))
+ .subscribe(grid.onClick, handleClick.bind(null, grid));
};
var handleClick = function (grid, event, args) {
if (grid.getColumns()[args.cell].id === 'row-header-column') {
var $rowHeaderSpan = $(event.target);
- if($rowHeaderSpan.data('cell-type') != "row-header-selector") {
+ if ($rowHeaderSpan.data('cell-type') != "row-header-selector") {
$rowHeaderSpan = $(event.target).find('[data-cell-type="row-header-selector"]');
}
- $rowHeaderSpan.toggleClass('selected');
+
+ $rowHeaderSpan.parent().toggleClass('selected');
updateRanges(grid, args.row);
}
};
- var handleSelectedRangesChanged = function (grid, event, ranges) {
- $('[data-cell-type="row-header-selector"].selected')
- .each(function (index, rowHeaderSpan) {
- var $rowHeaderSpan = $(rowHeaderSpan);
- var row = parseInt($rowHeaderSpan.data('row'));
- var isStillSelected = RangeSelectionHelper.isRangeSelected(ranges,
- RangeSelectionHelper.rangeForRow(grid, row));
- if (!isStillSelected) {
- $rowHeaderSpan.toggleClass('selected');
- }
- });
+ var handleSelectedRangesChanged = function (grid, event, selectedRanges) {
+ $('[data-cell-type="row-header-selector"]').each(function (index, rowHeaderSpan) {
+ var $rowHeaderSpan = $(rowHeaderSpan);
+ var row = parseInt($rowHeaderSpan.data('row'));
+
+ if (isRowSelected(grid, selectedRanges, row)) {
+ $rowHeaderSpan.parent().addClass('selected');
+ } else {
+ $rowHeaderSpan.parent().removeClass('selected');
+ }
+ });
};
var updateRanges = function (grid, rowId) {
@@ -55,6 +60,19 @@ define(['jquery', 'sources/selection/range_selection_helper', 'slickgrid'], func
selectionModel.setSelectedRanges(newRanges);
};
+ var isAnyCellSelectedInRow = function (grid, selectedRanges, row) {
+ var isStillSelected = RangeSelectionHelper.isRangeEntirelyWithinSelectedRanges(selectedRanges,
+ RangeSelectionHelper.rangeForRow(grid, row));
+ var cellSelectedInRow = RangeSelectionHelper.isAnyCellOfRowSelected(selectedRanges, row);
+
+ return isStillSelected || cellSelectedInRow;
+ };
+
+ var isRowSelected = function (grid, selectedRanges, row) {
+ var allRangesAreColumns = RangeSelectionHelper.areAllRangesCompleteColumns(grid, selectedRanges);
+ return isAnyCellSelectedInRow(grid, selectedRanges, row) && !allRangesAreColumns;
+ };
+
var getColumnDefinitions = function (columnDefinitions) {
columnDefinitions.unshift({
id: 'row-header-column',
diff --git a/web/pgadmin/static/js/selection/set_staged_rows.js b/web/pgadmin/static/js/selection/set_staged_rows.js
index 46aa8c8c..249a6414 100644
--- a/web/pgadmin/static/js/selection/set_staged_rows.js
+++ b/web/pgadmin/static/js/selection/set_staged_rows.js
@@ -10,9 +10,10 @@
define(
[
'jquery',
- 'underscore'
+ 'underscore',
+ 'sources/selection/range_selection_helper'
],
- function ($, _) {
+ function ($, _, RangeSelectionHelper) {
function disableButton(selector) {
$(selector).prop('disabled', true);
}
@@ -85,19 +86,19 @@ define(
disableButton('#btn-delete-row');
disableButton('#btn-copy-row');
- if (!_.has(this.selection, 'getSelectedRows')) {
- setStagedRows({});
- return;
+ function areAllSelectionsEntireRows() {
+ return RangeSelectionHelper.areAllRangesCompleteRows(self.grid,
+ self.selection.getSelectedRanges())
}
- var selectedRows = this.selection.getSelectedRows();
var selectedRanges = this.selection.getSelectedRanges();
if (selectedRanges.length > 0) {
enableButton('#btn-copy-row');
}
- if (selectedRows.length > 0) {
+ if (areAllSelectionsEntireRows()) {
+ var selectedRows = RangeSelectionHelper.getIndexesOfCompleteRows(this.grid, this.selection.getSelectedRanges())
var stagedRows = getPrimaryKeysForSelectedRows(self, selectedRows);
setStagedRows(stagedRows);
if (_.isEmpty(stagedRows)) {
@@ -114,5 +115,3 @@ define(
return setStagedRows;
}
);
-
-
diff --git a/web/pgadmin/static/js/selection/xcell_selection_model.js b/web/pgadmin/static/js/selection/xcell_selection_model.js
new file mode 100644
index 00000000..81f46cce
--- /dev/null
+++ b/web/pgadmin/static/js/selection/xcell_selection_model.js
@@ -0,0 +1,228 @@
+define([
+ 'jquery',
+ 'underscore',
+ 'sources/selection/range_selection_helper',
+
+ 'slickgrid',
+ 'slickgrid/plugins/slick.cellrangeselector'
+], function ($, _, RangeSelectionHelper) {
+ var XCellSelectionModel = function (options) {
+
+ var KEY_ARROW_RIGHT = 39;
+ var KEY_ARROW_LEFT = 37;
+ var KEY_ARROW_UP = 38;
+ var KEY_ARROW_DOWN = 40;
+
+ var _grid;
+ var _canvas;
+ var _ranges = [];
+ var _self = this;
+ var _selector = new Slick.CellRangeSelector({
+ "selectionCss": {
+ "border": "2px solid black"
+ }
+ });
+ var _options;
+ var _defaults = {
+ selectActiveCell: true
+ };
+
+
+ function init(grid) {
+ _options = $.extend(true, {}, _defaults, options);
+ _grid = grid;
+ _canvas = _grid.getCanvasNode();
+ _grid.onActiveCellChanged.subscribe(handleActiveCellChange);
+ _grid.onKeyDown.subscribe(handleKeyDown);
+ grid.registerPlugin(_selector);
+ _selector.onCellRangeSelected.subscribe(handleCellRangeSelected);
+ _selector.onBeforeCellRangeSelected.subscribe(handleBeforeCellRangeSelected);
+ $(window.parent).mouseup(handleWindowMouseUp);
+ }
+
+ function destroy() {
+ _grid.onActiveCellChanged.unsubscribe(handleActiveCellChange);
+ _grid.onKeyDown.unsubscribe(handleKeyDown);
+ _selector.onCellRangeSelected.unsubscribe(handleCellRangeSelected);
+ _selector.onBeforeCellRangeSelected.unsubscribe(handleBeforeCellRangeSelected);
+ _grid.unregisterPlugin(_selector);
+ $(window.parent).off('mouseup', handleWindowMouseUp);
+ }
+
+ function removeInvalidRanges(ranges) {
+ var result = [];
+
+ for (var i = 0; i < ranges.length; i++) {
+ var r = ranges[i];
+ if (_grid.canCellBeSelected(r.fromRow, r.fromCell) && _grid.canCellBeSelected(r.toRow, r.toCell)) {
+ result.push(r);
+ }
+ }
+
+ return result;
+ }
+
+ function setSelectedRanges(ranges) {
+ // simple check for: empty selection didn't change, prevent firing onSelectedRangesChanged
+ if ((!_ranges || _ranges.length === 0) && (!ranges || ranges.length === 0)) { return; }
+
+ _ranges = removeInvalidRanges(ranges);
+ _self.onSelectedRangesChanged.notify(_ranges);
+ }
+
+ function getSelectedRanges() {
+ return _ranges;
+ }
+
+ function setSelectedRows(rows) {
+ _ranges = [];
+
+ for(var i = 0 ; i < rows.length ; i++) {
+ _ranges.push(RangeSelectionHelper.rangeForRow(_grid, rows[i]));
+ }
+ }
+
+ function handleBeforeCellRangeSelected(e, args) {
+ if (_grid.getEditorLock().isActive()) {
+ e.stopPropagation();
+ return false;
+ }
+ }
+
+ function handleCellRangeSelected(e, args) {
+ setSelectedRanges([args.range]);
+ }
+
+ function handleActiveCellChange(e, args) {
+ if (_options.selectActiveCell && args.row != null && args.cell != null) {
+ setSelectedRanges([new Slick.Range(args.row, args.cell)]);
+ }
+ }
+
+ function arrowKeyPressed(event) {
+ return event.which == KEY_ARROW_RIGHT
+ || event.which == KEY_ARROW_LEFT
+ || event.which == KEY_ARROW_UP
+ || event.which == KEY_ARROW_DOWN;
+ }
+
+ function shiftArrowKeyPressed(event) {
+ return event.shiftKey && !event.ctrlKey && !event.altKey &&
+ (arrowKeyPressed(event));
+ }
+
+ function needUpdateRange(newRange) {
+ return removeInvalidRanges([newRange]).length;
+ }
+
+ function handleKeyDown(e) {
+ var ranges;
+ var lastSelectedRange;
+ var anchorActiveCell = _grid.getActiveCell();
+
+ function isKey(key) { return e.which === key; }
+
+ function getKeycode() { return e.which; }
+
+ function shouldScrollToBottommostRow() { return anchorActiveCell.row === newSelectedRange.fromRow; }
+
+ function shouldScrollToRightmostColumn() { return anchorActiveCell.cell === newSelectedRange.fromCell; }
+
+ function getMobileCellFromRange(range, activeCell) {
+ var mobileCell = {};
+
+ mobileCell.row = range.fromRow === activeCell.row ? range.toRow : range.fromRow;
+ mobileCell.cell = range.fromCell === activeCell.cell ? range.toCell : range.fromCell;
+
+ return mobileCell;
+ }
+
+ function getNewRange(rangeCorner, oppositeCorner) {
+ var newFromCell = rangeCorner.cell <= oppositeCorner.cell ? rangeCorner.cell : oppositeCorner.cell;
+ var newToCell = rangeCorner.cell <= oppositeCorner.cell ? oppositeCorner.cell : rangeCorner.cell;
+
+ var newFromRow = rangeCorner.row <= oppositeCorner.row ? rangeCorner.row : oppositeCorner.row;
+ var newToRow = rangeCorner.row <= oppositeCorner.row ? oppositeCorner.row : rangeCorner.row;
+
+ return new Slick.Range(
+ newFromRow,
+ newFromCell,
+ newToRow,
+ newToCell
+ );
+ }
+
+ if (anchorActiveCell && shiftArrowKeyPressed(e)) {
+ ranges = getSelectedRanges();
+ if (!ranges.length) {
+ ranges.push(new Slick.Range(anchorActiveCell.row, anchorActiveCell.cell));
+ }
+
+ // keyboard can work with last range only
+ lastSelectedRange = ranges.pop();
+
+ // can't handle selection out of active cell
+ if (!lastSelectedRange.contains(anchorActiveCell.row, anchorActiveCell.cell)) {
+ lastSelectedRange = new Slick.Range(anchorActiveCell.row, anchorActiveCell.cell);
+ }
+
+ var mobileCell = getMobileCellFromRange(lastSelectedRange, anchorActiveCell);
+
+ switch (getKeycode()) {
+ case KEY_ARROW_LEFT:
+ mobileCell.cell -= 1;
+ break;
+ case KEY_ARROW_RIGHT:
+ mobileCell.cell += 1;
+ break;
+ case KEY_ARROW_UP:
+ mobileCell.row -= 1;
+ break;
+ case KEY_ARROW_DOWN:
+ mobileCell.row += 1;
+ break;
+ }
+
+ var newSelectedRange = getNewRange(anchorActiveCell, mobileCell);
+
+ if (needUpdateRange(newSelectedRange)) {
+ var rowToView = shouldScrollToBottommostRow() ? newSelectedRange.toRow : newSelectedRange.fromRow;
+ var columnToView = shouldScrollToRightmostColumn() ? newSelectedRange.toCell : newSelectedRange.fromCell;
+
+ if (isKey(KEY_ARROW_RIGHT) || isKey(KEY_ARROW_LEFT)) {
+ _grid.scrollColumnIntoView(columnToView);
+ } else if (isKey(KEY_ARROW_UP) || isKey(KEY_ARROW_DOWN)) {
+ _grid.scrollRowIntoView(rowToView);
+ }
+ ranges.push(newSelectedRange);
+ } else {
+ ranges.push(lastSelectedRange);
+ }
+
+ setSelectedRanges(ranges);
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+
+ function handleWindowMouseUp(event) {
+ var selectedRange = _selector.getCurrentRange();
+ if (!_.isUndefined(selectedRange)) {
+ _grid.onDragEnd.notify({range: selectedRange});
+ }
+ }
+
+ $.extend(this, {
+ "getSelectedRanges": getSelectedRanges,
+ "setSelectedRanges": setSelectedRanges,
+ "setSelectedRows": setSelectedRows,
+
+ "init": init,
+ "destroy": destroy,
+
+ "onSelectedRangesChanged": new Slick.Event()
+ });
+ };
+ return XCellSelectionModel;
+});
diff --git a/web/pgadmin/static/vendor/slickgrid/plugins/slick.rowselectionmodel.js b/web/pgadmin/static/vendor/slickgrid/plugins/slick.rowselectionmodel.js
index 8553d050..95c12ab5 100644
--- a/web/pgadmin/static/vendor/slickgrid/plugins/slick.rowselectionmodel.js
+++ b/web/pgadmin/static/vendor/slickgrid/plugins/slick.rowselectionmodel.js
@@ -81,7 +81,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 = ranges;
_self.onSelectedRangesChanged.notify(_ranges);
diff --git a/web/pgadmin/static/vendor/slickgrid/slick.grid.js b/web/pgadmin/static/vendor/slickgrid/slick.grid.js
index 8ebfd68b..27e8009b 100644
--- a/web/pgadmin/static/vendor/slickgrid/slick.grid.js
+++ b/web/pgadmin/static/vendor/slickgrid/slick.grid.js
@@ -2794,6 +2794,25 @@ if (typeof Slick === "undefined") {
}
}
+ function scrollColumnIntoView(columnIndex) {
+ var colspan = getColspan(row, columnIndex);
+
+ var left = columnPosLeft[columnIndex],
+ right = columnPosRight[columnIndex + (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 scrollCellIntoView(row, cell, doPaging) {
scrollRowIntoView(row, doPaging);
@@ -3740,6 +3759,7 @@ if (typeof Slick === "undefined") {
"scrollRowIntoView": scrollRowIntoView,
"scrollRowToTop": scrollRowToTop,
"scrollCellIntoView": scrollCellIntoView,
+ "scrollColumnIntoView": scrollColumnIntoView,
"getCanvasNode": getCanvasNode,
"focus": setFocus,
diff --git a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
index 8695b11e..e4be9ebe 100644
--- a/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
+++ b/web/pgadmin/tools/sqleditor/static/css/sqleditor.css
@@ -260,7 +260,7 @@ li {
}
.slick-header-column.ui-state-default {
- height: 40px !important;
+ height: 32px !important;
}
#datagrid .grid-header label {
@@ -350,9 +350,19 @@ li {
#datagrid .slick-header .slick-header-column.ui-state-default {
color: #222222;
- padding: 4px 0 4px 6px;
- background-color: #e8e8e8;
- border-bottom: 1px solid black;
+ padding: 4px 0 3px 6px;
+ background-color: #E8E8E8;
+ border-bottom: 1px solid silver !important;
+}
+
+#datagrid .slick-header .slick-header-column.selected {
+ background-color: #2C76B4;
+ color: #FFFFFF;
+}
+
+#datagrid .slick-header .slick-header-column .column-name {
+ font-weight: bold;
+ display: block;
}
.column-description {
@@ -404,11 +414,6 @@ input.editor-checkbox:focus {
outline: none;
}
-/* Override selected row color */
-.slick-cell.selected {
- background-color: #DEE8F1 !important;
-}
-
/* Remove active cell border */
.slick-cell.active {
border: 1px solid transparent;
@@ -441,11 +446,20 @@ input.editor-checkbox:focus {
color: #999999;
}
+/* Override selected row color */
+#datagrid .slick-row .slick-cell.selected {
+ background-color: #DEE8F1;
+}
+
/* color the first column */
-.slick-row .slick-cell.l0.r0 {
+#datagrid .slick-row .slick-cell.l0.r0 {
background-color: #e8e8e8;
}
+#datagrid .slick-row .slick-cell.l0.r0.selected {
+ background-color: #2C76B4;
+}
+
#datagrid div.slick-header.ui-state-default {
background: #ffffff;
border-bottom: none;
@@ -464,11 +478,7 @@ input.editor-checkbox:focus {
-ms-box-sizing: content-box;
}
-.slick-row.ui-widget-content {
- border-top: 1px solid silver;
-}
-
.select-all-icon {
margin-left: 9px;
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 310c6f5b..b6bbfc43 100644
--- a/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js
+++ b/web/pgadmin/tools/sqleditor/templates/sqleditor/js/sqleditor.js
@@ -8,6 +8,7 @@ define(
'sources/selection/copy_data',
'sources/selection/range_selection_helper',
'sources/slickgrid/event_handlers/handle_query_output_keyboard_event',
+ 'sources/selection/xcell_selection_model',
'sources/selection/set_staged_rows',
'sources/gettext',
@@ -35,7 +36,7 @@ define(
function(
$, _, S, alertify, pgAdmin, Backbone, Backgrid, CodeMirror, pgExplain, GridSelector,
ActiveCellCapture, clipboard, copyData, RangeSelectionHelper, handleQueryOutputKeyboardEvent,
- setStagedRows, gettext
+ XCellSelectionModel, setStagedRows, gettext
) {
/* Return back, this has been called more than once */
if (pgAdmin.SqlEditor)
@@ -601,6 +602,8 @@ define(
pos: c.pos,
field: c.name,
name: c.label,
+ display_name: c.display_name,
+ column_type: c.column_type,
not_null: c.not_null,
has_default_val: c.has_default_val
};
@@ -686,7 +689,7 @@ define(
var grid = new Slick.Grid($data_grid, collection, grid_columns, grid_options);
grid.registerPlugin( new Slick.AutoTooltips({ enableForHeaderCells: false }) );
grid.registerPlugin(new ActiveCellCapture());
- grid.setSelectionModel(new Slick.CellSelectionModel());
+ grid.setSelectionModel(new XCellSelectionModel());
grid.registerPlugin(gridSelector);
var editor_data = {
@@ -2108,6 +2111,8 @@ define(
'cell': col_cell,
'can_edit': self.can_edit,
'type': type,
+ 'display_name': c.display_name,
+ 'column_type': col_type,
'not_null': c.not_null,
'has_default_val': c.has_default_val
};
diff --git a/web/regression/javascript/selection/column_selector_spec.js b/web/regression/javascript/selection/column_selector_spec.js
index 7e6f2a5f..679af52f 100644
--- a/web/regression/javascript/selection/column_selector_spec.js
+++ b/web/regression/javascript/selection/column_selector_spec.js
@@ -4,14 +4,14 @@ define(
"sources/selection/column_selector",
"sources/selection/active_cell_capture",
"sources/selection/grid_selector",
+ 'sources/selection/xcell_selection_model',
"slickgrid",
- 'slickgrid/slick.cellrangedecorator',
- 'slickgrid/slick.cellrangeselector',
- "slickgrid/slick.cellselectionmodel",
+ 'slickgrid/plugins/slick.cellrangedecorator',
+ 'slickgrid/plugins/slick.cellrangeselector',
"slickgrid/slick.grid",
],
- function ($, _, ColumnSelector, ActiveCellCapture, GridSelector) {
+ function ($, _, ColumnSelector, ActiveCellCapture, GridSelector, XCellSelectionModel) {
var KEY_RIGHT = 39;
var KEY_LEFT = 37;
var KEY_UP = 38;
@@ -31,6 +31,10 @@ define(
'some-column-name': 'first value',
'second column': 'second value',
'third column': 'nonselectable value'
+ }, {
+ 'some-column-name': 'row 1 - first value',
+ 'second column': 'row 1 - second value',
+ 'third column': 'row 1 - nonselectable value'
}];
columns = [
@@ -38,26 +42,36 @@ define(
id: 'row-header-column',
name: 'row header column name',
selectable: false,
+ display_name: 'row header column name',
+ column_type: 'text'
},
{
id: '1',
name: 'some-column-name',
- pos: 0
+ pos: 0,
+ display_name: 'some-column-name',
+ column_type: 'text'
},
{
id: '2',
name: 'second column',
- pos: 1
+ pos: 1,
+ display_name: 'second column',
+ column_type: 'json'
},
{
id: 'third-column-id',
name: 'third column',
- pos: 2
+ pos: 2,
+ display_name: 'third column',
+ column_type: 'text'
},
{
name: 'some-non-selectable-column',
selectable: false,
- pos: 3
+ pos: 3,
+ display_name: 'some-non-selectable-column',
+ column_type: 'numeric'
}
];
});
@@ -67,8 +81,12 @@ define(
expect($(container.find('.slick-header-columns .slick-column-name')[1]).text())
.toContain('some-column-name');
+ expect($(container.find('.slick-header-columns .slick-column-name')[1]).text())
+ .toContain('text');
expect($(container.find('.slick-header-columns .slick-column-name')[2]).text())
.toContain('second column');
+ expect($(container.find('.slick-header-columns .slick-column-name')[2]).text())
+ .toContain('json');
});
it("preserves the other attributes of column definitions", function () {
@@ -95,7 +113,7 @@ define(
grid = new SlickGrid(container, data, columns);
grid.registerPlugin(new ActiveCellCapture());
- cellSelectionModel = new Slick.CellSelectionModel();
+ cellSelectionModel = new XCellSelectionModel();
grid.setSelectionModel(cellSelectionModel);
grid.registerPlugin(columnSelector);
@@ -128,10 +146,10 @@ define(
describe("when the user clicks an additional column header", function () {
beforeEach(function () {
container.find('.slick-header-column:contains(some-column-name)').click();
+ container.find('.slick-header-column:contains(second column)').click();
});
it("selects additional columns", function () {
- container.find('.slick-header-column:contains(second column)').click();
var selectedRanges = cellSelectionModel.getSelectedRanges();
@@ -147,30 +165,34 @@ define(
describe("and presses shift + right-arrow", function () {
beforeEach(function () {
- container.find('.slick-header-column:contains(second column)').click();
+ pressShiftArrow(KEY_RIGHT);
});
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);
});
+
+ it("keeps selected class on columns 2 and 3", function () {
+ expect($(container.find('.slick-header-column:contains(second column)')).hasClass('selected'))
+ .toBe(true);
+ expect($(container.find('.slick-header-column:contains(third column)')).hasClass('selected'))
+ .toBe(true);
+ expect($(container.find('.slick-header-column:contains(some-column-name)')).hasClass('selected'))
+ .toBe(false);
+ });
});
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 () {
@@ -226,7 +248,7 @@ define(
expect(column.toCell).toBe(1);
expect(column.fromRow).toBe(0);
expect(column.toRow).toBe(9);
- })
+ });
});
describe("clicking a second time", function () {
@@ -266,7 +288,7 @@ define(
describe("when a non-column range was already selected", function () {
beforeEach(function () {
- var selectedRanges = [new Slick.Range(0, 0, 1, 0)];
+ var selectedRanges = [new Slick.Range(0, 0, 2, 0)];
cellSelectionModel.setSelectedRanges(selectedRanges);
});
@@ -275,7 +297,90 @@ define(
var selectedRanges = cellSelectionModel.getSelectedRanges();
expectOnlyTheFirstColumnToBeSelected(selectedRanges);
- })
+ });
+ });
+
+ describe('when a column is selected', function () {
+ beforeEach(function () {
+ container.find('.slick-header-column:contains(some-column-name)').click();
+ });
+
+ describe('when the user click a cell on the current range', function () {
+ beforeEach(function () {
+ container.find('.slick-cell.l1.r1')[1].click();
+ });
+
+ it('column is deselected', function () {
+
+ var selectedRanges = cellSelectionModel.getSelectedRanges();
+
+ expect(selectedRanges.length).toBe(1);
+
+ var column = selectedRanges[0];
+
+ expect(column.fromCell).toBe(1);
+ expect(column.toCell).toBe(1);
+ expect(column.fromRow).toBe(1);
+ expect(column.toRow).toBe(1);
+ });
+
+ it('keep select class on column header', function () {
+ expect($(container.find('.slick-header-column:contains(some-column-name)')).hasClass('selected'))
+ .toBeTruthy();
+ });
+ });
+
+ describe('when the user click a cell outside the current range', function () {
+ beforeEach(function () {
+ container.find('.slick-cell.l2.r2')[2].click();
+ });
+
+ it('column is deselected', function () {
+
+ var selectedRanges = cellSelectionModel.getSelectedRanges();
+
+ expect(selectedRanges.length).toBe(1);
+
+ var column = selectedRanges[0];
+
+ expect(column.fromCell).toBe(2);
+ expect(column.toCell).toBe(2);
+ expect(column.fromRow).toBe(2);
+ expect(column.toRow).toBe(2);
+ });
+
+ it('remove select class on "some-column-name" column header', function () {
+ expect($(container.find('.slick-header-column:contains(some-column-name)')).hasClass('selected'))
+ .toBeFalsy();
+ expect($(container.find('.slick-header-column:contains(second column)')).hasClass('selected'))
+ .toBeTruthy();
+ });
+ });
+
+ describe('when the user click in a row header', function () {
+ beforeEach(function () {
+ var selectedRanges = [new Slick.Range(1, 1, 1, 3)];
+ cellSelectionModel.setSelectedRanges(selectedRanges);
+ });
+
+ it('column is deselected', function () {
+ var selectedRanges = cellSelectionModel.getSelectedRanges();
+
+ expect(selectedRanges.length).toBe(1);
+
+ var column = selectedRanges[0];
+
+ expect(column.fromCell).toBe(1);
+ expect(column.toCell).toBe(3);
+ expect(column.fromRow).toBe(1);
+ expect(column.toRow).toBe(1);
+ });
+
+ it('no column should have the class "selected"', function () {
+ expect($(container.find('.slick-header-column:contains(some-column-name)')).hasClass('selected'))
+ .toBeFalsy();
+ });
+ });
});
});
@@ -284,7 +389,7 @@ define(
columns = columnSelector.getColumnDefinitions(columns);
var grid = new SlickGrid(container, data, columns, options);
- var cellSelectionModel = new Slick.CellSelectionModel();
+ var cellSelectionModel = new XCellSelectionModel();
grid.setSelectionModel(cellSelectionModel);
grid.registerPlugin(columnSelector);
diff --git a/web/regression/javascript/selection/copy_data_spec.js b/web/regression/javascript/selection/copy_data_spec.js
index 718a830b..561576ce 100644
--- a/web/regression/javascript/selection/copy_data_spec.js
+++ b/web/regression/javascript/selection/copy_data_spec.js
@@ -10,21 +10,29 @@
define(
["jquery",
"slickgrid/slick.grid",
- "slickgrid/slick.rowselectionmodel",
+ "sources/selection/xcell_selection_model",
"sources/selection/copy_data",
"sources/selection/clipboard",
"sources/selection/range_selection_helper"
],
- function ($, SlickGrid, RowSelectionModel, copyData, clipboard, RangeSelectionHelper) {
+ function ($, SlickGrid, XCellSelectionModel, copyData, clipboard, RangeSelectionHelper) {
describe('copyData', function () {
- var grid, sqlEditor;
+ var grid, sqlEditor, gridContainer, buttonPasteRow;
beforeEach(function () {
var data = [[1, "leopord", "12"],
[2, "lion", "13"],
[3, "puma", "9"]];
- var columns = [{
+ var columns = [
+ {
+ id: 'row-header-column',
+ name: 'row header column name',
+ selectable: false,
+ display_name: 'row header column name',
+ column_type: 'text'
+ },
+ {
name: "id",
pos: 0,
label: "id
numeric",
@@ -48,17 +56,18 @@ define(
}
]
;
- var gridContainer = $("