Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Multiple checkbox selection with Shift key #1750

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -25,6 +25,7 @@ import {
isCheckBoxedTable,
recordId,
rowId,
rowIndex,
tablePanelView
} from "../renderingValues";
import {
Expand All @@ -44,14 +45,15 @@ import {
topTextOffset
} from "./cellsCommon";
import { CPR } from "utils/canvas";
import { onClick } from "../onClick";
import { onClick, onMouseMove } from "../onClick";
import { getDataTable } from "model/selectors/DataView/getDataTable";
import { getSelectionMember } from "model/selectors/DataView/getSelectionMember";
import { getDataSourceFieldByName } from "model/selectors/DataSources/getDataSourceFieldByName";
import { getFormScreenLifecycle } from "model/selectors/FormScreen/getFormScreenLifecycle";
import { flow } from "mobx";
import { hasSelectedRowId, setSelectedStateRowId, } from "model/actions-tree/selectionCheckboxes";
import { getSelectedRowId } from "model/selectors/TablePanelView/getSelectedRowId";
import { getTablePanelView } from "model/selectors/TablePanelView/getTablePanelView";

export const selectionCheckBoxColumnWidth = 20;

Expand All @@ -67,10 +69,28 @@ export function selectionCheckboxCellsDraws() {
drawSelectionCheckboxBackground();
const ctx2d = context2d();
ctx2d.fillStyle = "black";
ctx2d.font = `${CPR() * checkBoxCharacterFontSize}px "Font Awesome 5 Free"`;
const state = dataView().isSelected(rowId());

const {
selectionRangeIndex0,
selectionRangeIndex1,
selectionInProgress,
selectionTargetState
} = tablePanelView();

const isSelectionCandidate =
selectionInProgress
&& selectionRangeIndex0 !== undefined
&& selectionRangeIndex1 !== undefined
&& Math.min(selectionRangeIndex0, selectionRangeIndex1) <= rowIndex()
&& rowIndex() <= Math.max(selectionRangeIndex0, selectionRangeIndex1)

ctx2d.font = `${(isSelectionCandidate) ? 'bold' : ""} ${CPR() * checkBoxCharacterFontSize}px "Font Awesome 5 Free"`;

ctx2d.fillText(
state ? "\uf14a" : "\uf0c8",
((!isSelectionCandidate && state) ||
(isSelectionCandidate && selectionTargetState) )
? "\uf14a" : "\uf0c8",
CPR() * (currentColumnLeft() + checkBoxCellPaddingLeft),
CPR() * (currentRowTop() + topTextOffset)
);
Expand All @@ -90,28 +110,27 @@ function registerClickHandler() {
h: currentRowHeight(),
handler(event: any) {
flow(function*() {
// TODO: Move to tablePanelView
let newSelectionState = false;
const tablePanelView = getTablePanelView(ctx);
const dataTable = getDataTable(ctx);
const rowId = dataTable.getRowId(row);
const selectionMember = getSelectionMember(ctx);
if (!!selectionMember) {
const dataSourceField = getDataSourceFieldByName(ctx, selectionMember);
if (dataSourceField) {
newSelectionState = !dataTable.getCellValueByDataSourceField(row, dataSourceField);
dataTable.setDirtyValue(row, selectionMember, newSelectionState);
yield*getFormScreenLifecycle(ctx).onFlushData();
const updatedRow = dataTable.getRowById(rowId)!;
newSelectionState = dataTable.getCellValueByDataSourceField(updatedRow, dataSourceField);
yield*setSelectedStateRowId(ctx)(rowId, newSelectionState);
}
} else {
newSelectionState = !hasSelectedRowId(ctx, rowId);
yield*setSelectedStateRowId(ctx)(rowId, newSelectionState);
}
yield* tablePanelView.onSelectionCellClick(event, row, rowId)
})();
},
});
onMouseMove({
x: currentColumnLeftVisible(),
y: currentRowTop(),
w: currentColumnWidthVisible(),
h: currentRowHeight(),
handler(event: any) {
flow(function*() {
const tablePanelView = getTablePanelView(ctx);
const dataTable = getDataTable(ctx);
const rowId = dataTable.getRowId(row);
yield* tablePanelView.onSelectionCellMouseMove(event, row, rowId)
})();
}
})
}

export function selectionCheckboxEmptyCellsWidths() {
Expand Down
Expand Up @@ -19,7 +19,7 @@ along with ORIGAM. If not, see <http://www.gnu.org/licenses/>.

/* eslint-disable @typescript-eslint/no-unused-vars */
import bind from "bind-decorator";
import { action, computed, observable } from "mobx";
import { action, computed, flow, observable } from "mobx";
import { inject, observer, Provider } from "mobx-react";
import { onTableKeyDown } from "model/actions-ui/DataView/TableView/onTableKeyDown";
import React, { useContext } from "react";
Expand Down Expand Up @@ -102,6 +102,40 @@ export class TableViewInner extends React.Component<ITableViewProps & { dataView
tablePanelView.triggerOnFocusTable();
}
}

window.addEventListener('mousemove', this.handleWindowMouseMove);
window.addEventListener('keydown', this.handleWindowKeyDown);
window.addEventListener('keyup', this.handleWindowKeyUp);
}

componentWillUnmount() {
window.removeEventListener('mousemove', this.handleWindowMouseMove);
window.removeEventListener('keydown', this.handleWindowKeyDown);
window.removeEventListener('keyup', this.handleWindowKeyUp);
}

@action.bound handleWindowMouseMove(event: any) {
const thisInstance = this;
flow(function* () {
if(thisInstance.props.tablePanelView)
yield* thisInstance.props.tablePanelView.onWindowMouseMove(event)
})();
}

@action.bound handleWindowKeyDown(event: any) {
const thisInstance = this;
flow(function* () {
if(thisInstance.props.tablePanelView)
yield* thisInstance.props.tablePanelView.onWindowKeyDown(event)
})();
}

@action.bound handleWindowKeyUp(event: any) {
const thisInstance = this;
flow(function* () {
if(thisInstance.props.tablePanelView)
yield* thisInstance.props.tablePanelView.onWindowKeyUp(event)
})();
}

refTableDisposer: any;
Expand Down
12 changes: 8 additions & 4 deletions frontend-html/src/model/entities/DataView.ts
Expand Up @@ -333,18 +333,22 @@ export class DataView implements IDataView {
}
}

@computed get selectedRowIndex(): number | undefined {
getRowIndexById(rowId: any) {
if (getGroupingConfiguration(this).isGrouping) {
return getGrouper(this).allGroups.some((group) => group.isExpanded)
? getGrouper(this).getRowIndex(this.selectedRowId!)
? getGrouper(this).getRowIndex(rowId)
: undefined;
} else {
return this.selectedRowId
? this.dataTable.getExistingRowIdxById(this.selectedRowId)
return (rowId !== null && rowId !== undefined)
? this.dataTable.getExistingRowIdxById(rowId)
: undefined;
}
}

@computed get selectedRowIndex(): number | undefined {
return this.getRowIndexById(this.selectedRowId)
}

@computed get trueSelectedRowIndex(): number | undefined {
if (getGroupingConfiguration(this).isGrouping) {
return getGrouper(this).allGroups.some((group) => group.isExpanded)
Expand Down
160 changes: 160 additions & 0 deletions frontend-html/src/model/entities/TablePanelView/TablePanelView.tsx
Expand Up @@ -52,6 +52,11 @@ import { IConfigurationManager } from "model/entities/TablePanelView/types/IConf
import { isMobileLayoutActive } from "model/selectors/isMobileLayoutActive";
import { ColumnConfigurationModel } from "model/entities/TablePanelView/ColumnConfigurationModel";
import { getOpenedScreen } from "model/selectors/getOpenedScreen";
import { getSelectionMember } from "model/selectors/DataView/getSelectionMember";
import { getDataSourceFieldByName } from "model/selectors/DataSources/getDataSourceFieldByName";
import { getFormScreenLifecycle } from "model/selectors/FormScreen/getFormScreenLifecycle";
import { hasSelectedRowId, setSelectedStateRowId } from "model/actions-tree/selectionCheckboxes";
import { isLazyLoading } from "model/selectors/isLazyLoading";

export class TablePanelView implements ITablePanelView {
$type_ITablePanelView: 1 = 1;
Expand Down Expand Up @@ -266,6 +271,161 @@ export class TablePanelView implements ITablePanelView {
}
}


lastSelectionRowIdUnderMouse: any = undefined;
windowMouseMoveDeadPeriod = false;
@observable shiftPressed = false;
@observable selectionCellHoveredId: any = undefined;
@observable selectionTargetState: boolean = true;
@observable lastSelectedRowId: any = undefined;

@computed get isMultiSelectEnabled() {
return !(
this.groupingConfiguration.isGrouping ||
isLazyLoading(this) && !!getSelectionMember(this)
)
}

@computed get selectionInProgress() {
return this.isMultiSelectEnabled && this.shiftPressed;
}

@computed get selectionRangeIndex0() {
if(this.isMultiSelectEnabled && this.lastSelectedRowId !== undefined) {
const dataTable = getDataTable(this);
return dataTable.getExistingRowIdxById(this.lastSelectedRowId)
} else {
return undefined;
}
}

@computed get selectionRangeIndex1() {
if(this.isMultiSelectEnabled && this.selectionCellHoveredId !== undefined) {
const dataTable = getDataTable(this);
return dataTable.getExistingRowIdxById(this.selectionCellHoveredId);
} else {
return undefined;
}
}

windowMouseMoveDeadPeriodTimerHandle: any;
*onSelectionCellMouseMove(event: any, row: any[], rowId: any) {
if(this.lastSelectionRowIdUnderMouse !== rowId) {
if(this.lastSelectionRowIdUnderMouse) {
yield* this.onSelectionCellMouseOut(event, this.lastSelectionRowIdUnderMouse)
}
yield* this.onSelectionCellMouseIn(event, rowId)
}
this.lastSelectionRowIdUnderMouse = rowId;
this.windowMouseMoveDeadPeriod = true;
clearTimeout(this.windowMouseMoveDeadPeriodTimerHandle);
this.windowMouseMoveDeadPeriodTimerHandle = setTimeout(() => {
this.windowMouseMoveDeadPeriod = false;
}, 0)
}

*onSelectionCellClick(event: any, row: any[], rowId: any) {
const dataTable = getDataTable(this);
const rowsToSelect: {id: any, row: any[]}[] = [];
if(this.isMultiSelectEnabled && event.shiftKey && this.lastSelectedRowId !== undefined) {
const rowRangeStart = dataTable.getExistingRowIdxById(this.lastSelectedRowId);
const rowRangeEnd = dataTable.getExistingRowIdxById(rowId);
if(rowRangeStart !== undefined && rowRangeEnd !== undefined) {
for(
let i = Math.min(rowRangeStart, rowRangeEnd);
i <= Math.max(rowRangeStart, rowRangeEnd);
i++
) {
const rowItem = dataTable.getRowByExistingIdx(i)
const rowItemId = dataTable.getRowId(rowItem);
rowsToSelect.push({row: rowItem, id: rowItemId})
}
} else {
rowsToSelect.push({row, id: rowId})
}
} else {
rowsToSelect.push({row, id: rowId});
}

if(rowsToSelect.length > 0) {
this.lastSelectedRowId = rowsToSelect.slice(-1)[0].id;
}

const newSelectionState = rowsToSelect.length === 1
? !this.getIsRowSelected(rowId, row)
: this.selectionTargetState;

if (rowsToSelect.length === 1) {
this.selectionTargetState = newSelectionState;
}

const selectionMember = getSelectionMember(this);
if (!!selectionMember) {
const dataSourceField = getDataSourceFieldByName(this, selectionMember);
if (dataSourceField) {
for (let rowToSelect of rowsToSelect) {
dataTable.setDirtyValue(rowToSelect.row, selectionMember, newSelectionState);
}
yield*getFormScreenLifecycle(this).onFlushData();
for (let rowToSelect of rowsToSelect) {
const updatedRow = dataTable.getRowById(rowToSelect.id)!;
const updatedSelectionState = dataTable.getCellValueByDataSourceField(updatedRow, dataSourceField);
yield*setSelectedStateRowId(this)(rowToSelect.id, updatedSelectionState);
}
}
} else {
for (let rowToSelect of rowsToSelect) {
yield*setSelectedStateRowId(this)(rowToSelect.id, newSelectionState);
}
}
}


getIsRowSelected(rowId: any, row: any[]) {
const dataTable = getDataTable(this);
const selectionMember = getSelectionMember(this);
if(!!selectionMember) {
const dataSourceField = getDataSourceFieldByName(this, selectionMember);
return !!dataSourceField && dataTable.getCellValueByDataSourceField(row, dataSourceField);
} else {
return hasSelectedRowId(this, rowId)
}
}


*onWindowMouseMove(event: any) {
if(!event.shiftKey) {
this.shiftPressed = false;
}
if(!this.windowMouseMoveDeadPeriod) {
if(this.lastSelectionRowIdUnderMouse !== undefined) {
yield* this.onSelectionCellMouseOut(event, this.lastSelectionRowIdUnderMouse)
this.lastSelectionRowIdUnderMouse = undefined;
}
}
}

*onSelectionCellMouseIn(event: any, rowId: any) {
this.selectionCellHoveredId = rowId;
}

*onSelectionCellMouseOut(event: any, rowId: any) {
this.selectionCellHoveredId = undefined;
}

*onWindowKeyDown(event: any) {
if(event.key === 'Shift') {
this.shiftPressed = true;
}
}

*onWindowKeyUp(event: any) {
if(event.key === 'Shift') {
this.shiftPressed = false;
}
}


@action.bound
handleTableScroll(event: any, scrollTop: number, scrollLeft: number) {
if (!this.handleScrolling) {
Expand Down
Expand Up @@ -78,6 +78,24 @@ export interface ITablePanelView extends ITablePanelViewData {

onNoCellClick(): Generator;

onSelectionCellMouseMove(event: any, row: any[], rowId: any): Generator;

onSelectionCellClick(event: any, row: any[], rowId: any): Generator;

selectionRangeIndex0: number | undefined;
selectionRangeIndex1: number | undefined;
shiftPressed: boolean;
selectionTargetState: boolean;
selectionInProgress: boolean;

onWindowMouseMove(event: any): Generator;

onWindowKeyDown(event: any): Generator;

onWindowKeyUp(event: any): Generator;

selectionCellHoveredId: any;

onOutsideTableClick(): Generator;

dontHandleNextScroll(): void;
Expand Down
2 changes: 2 additions & 0 deletions frontend-html/src/model/entities/types/IDataView.ts
Expand Up @@ -161,6 +161,8 @@ export interface IDataView extends IDataViewData {

deleteRowAndSelectNext(row: any[]): void;

getRowIndexById(rowId: any): number | undefined;

clear(): void;

navigateLookupLink(property: IProperty, row: any[]): Generator<any>;
Expand Down