@eclipse-scout/core
Version:
Eclipse Scout runtime
244 lines (217 loc) • 9.12 kB
text/typescript
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import $ from 'jquery';
import {scout, Table, TableRow} from '../index';
/**
* Enhances the table with selection behaviour.<p>
*
* If mouseMoveSelectionEnabled is set to true, the user can select the rows by moving the mouse with pressed left mouse button.
*
*/
export class TableSelectionHandler {
table: Table;
mouseMoveSelectionEnabled: boolean;
lastActionRow: TableRow;
mouseOverHandler: (event: JQuery.MouseOverEvent) => void;
select: boolean;
counterDebug: number;
fromIndex: number;
toIndex: number;
protected _mouseDown: boolean;
/** Index of the row that got a 'mouseover' event previously (needed to determine if the user is going up or down) */
protected _prevSelectedRowIndex: number;
/** The index of the selected row with the greatest distance to fromIndex (needed to efficiently clear the selection) */
protected _maxSelectedRowIndex: number;
constructor(table: Table) {
this.table = table;
this.mouseMoveSelectionEnabled = true;
this._mouseDown = false;
this.lastActionRow = null;
this.mouseOverHandler = null;
this.select = true;
this.counterDebug = 0;
this.fromIndex = -1;
this.toIndex = -1;
this._prevSelectedRowIndex = -1;
this._maxSelectedRowIndex = -1;
}
clearLastSelectedRowMarker() {
this.lastActionRow = null;
}
// TODO [7.0] bsh: Table Selection | Try to merge this with TableKeystrokeContext
onMouseDown(event: JQuery.MouseDownEvent) {
let $row = $(event.currentTarget) as JQuery;
let row = $row.data('row') as TableRow;
let oldSelectedState = $row.isSelected();
let rows = this.table.visibleRows;
this._mouseDown = true;
this.select = true;
if (this.table.multiSelect && event.shiftKey) {
// when a selected row in the middle of a selection-block has
// been clicked while shift is pressed -> do nothing
if (this.table.selectedRows.indexOf(row) > -1) {
return;
}
if (this.table.selectedRows.length === 0) {
// Shift-click was pressed without selection -> behave like normal click
this.lastActionRow = row;
}
if (!this.lastActionRow) {
// The last action row may have been cleared, e.g. when rows have been replaced. In that case, simply assume
// the first or the last of the currently selected rows as being the last action row to make shift-click
// behave as expected (depending on which row is nearer from the clicked row).
let thisRowIndex = rows.indexOf(row);
let firstSelectedRow = this.table.selectedRows[0];
let lastSelectedRow = this.table.selectedRows[this.table.selectedRows.length - 1];
if (thisRowIndex <= (rows.indexOf(firstSelectedRow) + rows.indexOf(lastSelectedRow)) / 2) {
this.lastActionRow = firstSelectedRow;
} else {
this.lastActionRow = lastSelectedRow;
}
this._maxSelectedRowIndex = rows.indexOf(lastSelectedRow);
this._prevSelectedRowIndex = rows.indexOf(this.lastActionRow);
}
this.fromIndex = rows.indexOf(this.lastActionRow);
} else if (event.ctrlKey) {
this.select = !oldSelectedState;
} else {
// Click on the already selected row must not clear the selection it to avoid another selection event sent to the server
// Right click on already selected rows must not clear the selection
if (!oldSelectedState || (this.table.selectedRows.length > 1 && event.which !== 3)) {
this.table._removeSelection();
this.table.selectedRows = [];
}
}
if (this.fromIndex < 0) {
this.fromIndex = rows.indexOf(row);
}
if (event.which !== 3 || !oldSelectedState) {
this.toIndex = rows.indexOf(row);
this.handleSelection(event);
this.table.notifyRowSelectionFinished();
}
if (this.mouseMoveSelectionEnabled && event.which !== 3) {
this.table.$data.off('mouseover', this.mouseOverHandler);
this.mouseOverHandler = this.onMouseOver.bind(this);
this.table.$data.on('mouseover', '.table-row', this.mouseOverHandler);
// This additionally window listener is necessary to track the clicks outside of a table row.
// If the mouse is released on a table row, onMouseUp gets called by the table's mouseUp listener.
}
$row.window().one('mouseup.selectionHandler', this.onMouseUp.bind(this));
this.lastActionRow = row;
}
onMouseOver(event: JQuery.MouseOverEvent) {
let $row = $(event.currentTarget) as JQuery;
let row = $row.data('row') as TableRow;
let rows = this.table.visibleRows;
this.toIndex = rows.indexOf(row);
this.handleSelection(event);
this.lastActionRow = row;
}
handleSelection(event: JQuery.MouseEventBase) {
let rowsToUnselect: TableRow[];
let rows = this.table.visibleRows;
if (this.table.multiSelect) {
// Multi-selection -> expand/shrink selection
let thisIndex = this.toIndex;
let goingUp = (thisIndex < this._prevSelectedRowIndex);
let goingDown = (thisIndex > this._prevSelectedRowIndex);
let beforeFromSelection = (this._prevSelectedRowIndex < this.fromIndex);
let afterFromSelection = (this._prevSelectedRowIndex > this.fromIndex);
// In 'ctrlKey' mode, the un-selection is done via 'select=false'
// Also prevent unselect in shiftKey mode, because otherwise we'd could
// possibly have unwanted gaps within the selection block (see #172929).
if (!event.ctrlKey) {
// If we are going _towards_ the startIndex, unselect all rows between the current row and the
// selected row with the greatest distance (this._maxSelectedRowIndex).
if (goingUp && afterFromSelection) {
rowsToUnselect = rows.slice(thisIndex + 1, this._maxSelectedRowIndex + 1);
} else if (goingDown && beforeFromSelection) {
rowsToUnselect = rows.slice(this._maxSelectedRowIndex, thisIndex);
}
// when shift is pressed: only unselect when first or last row (but not in the middle of the selection, see #172929)
if (rowsToUnselect && event.shiftKey) {
let selectionIndizes = this.getMinMaxSelectionIndizes();
rowsToUnselect = rowsToUnselect.reduce((aggr, row) => {
let rowIndex = rows.indexOf(row);
if (scout.isOneOf(rowIndex, selectionIndizes[0], selectionIndizes[1])) {
aggr.push(row);
}
return aggr;
}, []);
}
if (rowsToUnselect) {
rowsToUnselect.forEach(row => this.table.removeRowFromSelection(row, true));
}
}
// Adjust the indexes
this._maxSelectedRowIndex = (goingUp ? Math.min(this._maxSelectedRowIndex, thisIndex) : (goingDown ? Math.max(this._maxSelectedRowIndex, thisIndex) : thisIndex));
this._prevSelectedRowIndex = thisIndex;
} else {
// Single selection -> unselect previously selected row
if (this.select) {
this.table._removeSelection();
this.table.selectedRows = [];
}
// Adjust the indexes
this.fromIndex = this.toIndex;
}
// Set the new selection
this._selectRange(this.fromIndex, this.toIndex, this.select);
}
protected _selectRange(fromIndex: number, toIndex: number, select: boolean) {
let rows = this.table.visibleRows;
let startIndex = Math.min(fromIndex, toIndex);
let endIndex = Math.max(fromIndex, toIndex) + 1;
let actionRows = rows.slice(startIndex, endIndex);
// set/remove selection
if (select) {
actionRows.forEach(row => this.table.addRowToSelection(row, true));
} else {
actionRows.forEach(row => this.table.removeRowFromSelection(row, true));
}
}
getMinMaxSelectionIndizes(): number[] {
let
selectedRows = this.table.selectedRows,
allRows = this.table.visibleRows;
if (!selectedRows || selectedRows.length === 0) {
return [-1, -1];
}
let min = -1, max = -1;
selectedRows.forEach(row => {
let index = allRows.indexOf(row);
if (min === -1 || index < min) {
min = index;
}
if (max === -1 || index > max) {
max = index;
}
});
return [min, max];
}
onMouseUp(event: JQuery.MouseUpEvent) {
if (!this._mouseDown) {
// May happen when selecting elements with chrome dev tools
return;
}
if (!this.table.rendered) {
// May happen when the table is removed between the mouse down and the mouse up event
// (e.g. when the user clicks 3 times very fast --> table is removed after double click).
return;
}
this._mouseDown = false;
this.table.$data.off('mouseover', this.mouseOverHandler);
this.fromIndex = -1;
this.toIndex = -1;
this.select = true;
this.table.notifyRowSelectionFinished();
}
}