1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-03 16:10:26 +00:00
Files
seahub/frontend/src/components/sf-table/masks/interaction-masks/index.js
2025-01-07 12:17:57 +08:00

1142 lines
40 KiB
JavaScript

import React, { isValidElement, cloneElement } from 'react';
import PropTypes from 'prop-types';
import deepCopy from 'deep-copy';
import toaster from '../../../toast';
import EditorPortal from '../../editors/editor-portal';
import EditorContainer from '../../editors/editor-container';
import DragHandler from '../drag-handler';
import DragMask from '../drag-mask';
import SelectionRangeMask from '../selection-range-mask';
import SelectionMask from '../selection-mask';
import { gettext } from '../../../../utils/constants';
import { Utils } from '../../../../utils/utils';
import { isEmptyObject } from '../../../../utils/object';
import { KeyCodes } from '../../../../constants';
import { GROUP_ROW_TYPE } from '../../constants/group';
import TRANSFER_TYPES from '../../constants/transfer-types';
import { GRID_HEADER_DOUBLE_HEIGHT, GRID_HEADER_DEFAULT_HEIGHT, HEADER_HEIGHT_TYPE, PASTE_SOURCE, EDITOR_TYPE } from '../../constants/grid';
import {
getNewSelectedRange, getSelectedDimensions, selectedRangeIsSingleCell,
getSelectedRangeDimensions, getSelectedRow, getSelectedColumn,
getRecordsFromSelectedRange, getSelectedCellValue, checkIsSelectedCellEditable,
} from '../../utils/selected-cell-utils';
import RecordMetrics from '../../utils/record-metrics';
import setEventTransfer from '../../utils/set-event-transfer';
import getEventTransfer from '../../utils/get-event-transfer';
import { getGroupRecordByIndex } from '../../utils/group-metrics';
import { isSpace } from '../../../../utils/hotkey';
import EventBus from '../../../common/event-bus';
import { EVENT_BUS_TYPE } from '../../constants/event-bus-type';
import { isCtrlKeyHeldDown, isKeyPrintable } from '../../../../utils/keyboard-utils';
import { checkIsColumnSupportPreview, getColumnIndexByKey } from '../../utils/column';
import './index.css';
class InteractionMasks extends React.Component {
static defaultProps = {
enableCellSelect: true,
isGroupView: false,
groupOffsetLeft: 0,
};
throttle = null;
constructor(props) {
super(props);
const initPosition = { idx: -1, rowIdx: -1, groupRecordIndex: -1 };
this.state = {
selectedPosition: initPosition,
selectedRange: {
topLeft: initPosition,
bottomRight: initPosition,
startCell: null,
cursorCell: null,
isDragging: false,
},
draggedRange: null,
isEditorEnabled: false,
openEditorMode: '',
};
this.eventBus = EventBus.getInstance();
this.pasteSource = PASTE_SOURCE.COPY;
this.cutPosition = null;
this.selectionMask = null;
}
componentDidMount() {
this.unsubscribeSelectColumn = this.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_COLUMN, this.onColumnSelect);
this.unsubscribeDragEnter = this.eventBus.subscribe(EVENT_BUS_TYPE.DRAG_ENTER, this.handleDragEnter);
this.unsubscribeSelectCell = this.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_CELL, this.onSelectCell);
this.unsubscribeSelectNone = this.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_NONE, this.selectNone);
this.unsubscribeSelectStart = this.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_START, this.onSelectCellRangeStarted);
this.unsubscribeSelectUpdate = this.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_UPDATE, this.onSelectCellRangeUpdated);
this.unsubscribeSelectEnd = this.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_END, this.onSelectCellRangeEnded);
this.unsubscribeOpenEditorEvent = this.eventBus.subscribe(EVENT_BUS_TYPE.OPEN_EDITOR, this.onOpenEditorEvent);
this.unsubscribeCloseEditorEvent = this.eventBus.subscribe(EVENT_BUS_TYPE.CLOSE_EDITOR, this.onCloseEditorEvent);
this.unsubscribeCopy = this.eventBus.subscribe(EVENT_BUS_TYPE.COPY_CELLS, this.onCopy);
this.unsubscribePaste = this.eventBus.subscribe(EVENT_BUS_TYPE.PASTE_CELLS, this.onPaste);
this.unsubscribeCut = this.eventBus.subscribe(EVENT_BUS_TYPE.CUT_CELLS, this.onCut);
}
componentDidUpdate(prevProps, prevState) {
const { selectedRange, isEditorEnabled } = this.state;
const { selectedRange: prevSelectedRange, isEditorEnabled: prevIsEditorEnabled } = prevState;
const isEditorClosed = isEditorEnabled !== prevIsEditorEnabled && !isEditorEnabled;
const isSelectedRangeChanged = selectedRange !== prevSelectedRange && (selectedRange.topLeft !== prevSelectedRange.topLeft || selectedRange.bottomRight !== prevSelectedRange.bottomRight);
if (isSelectedRangeChanged || isEditorClosed) {
this.focus();
}
}
componentWillUnmount() {
this.unsubscribeSelectColumn();
this.unsubscribeSelectCell();
this.unsubscribeSelectStart();
this.unsubscribeSelectUpdate();
this.unsubscribeSelectEnd();
this.unsubscribeOpenEditorEvent();
this.unsubscribeCloseEditorEvent();
this.unsubscribeCopy();
this.unsubscribePaste();
this.unsubscribeCut();
this.setState = (state, callback) => {
return;
};
}
onColumnSelect = (column) => {
const { columns, isGroupView, recordsCount } = this.props;
if (isGroupView) return;
const selectColumnIndex = getColumnIndexByKey(column.key, columns);
this.setState({
selectedPosition: { ...this.state.selectedPosition, idx: selectColumnIndex, rowIdx: 0 },
selectedRange: {
startCell: { idx: selectColumnIndex, rowIdx: 0 },
topLeft: { idx: selectColumnIndex, rowIdx: 0 },
bottomRight: { idx: selectColumnIndex, rowIdx: recordsCount - 1 },
isDragging: false,
}
});
};
onOpenEditorEvent = (mode) => {
this.setState({ openEditorMode: mode }, () => {
this.openEditor(null);
});
};
onCloseEditorEvent = () => {
if (this.state.isEditorEnabled) {
this.closeEditor();
}
};
onSelectCell = (cell, openEditor) => {
const { selectedPosition, isEditorEnabled } = this.state;
const callback = openEditor ? this.openEditor : () => null;
if (isEditorEnabled) {
this.closeEditor();
}
this.setState((prevState) => {
const next = { ...selectedPosition, ...cell };
if (this.isCellWithinBounds(next)) {
return {
selectedPosition: next,
selectedRange: {
topLeft: next,
bottomRight: next,
startCell: next,
cursorCell: next,
isDragging: false,
}
};
}
return prevState;
}, callback);
};
selectNone = () => {
const initPosition = { idx: -1, rowIdx: -1, groupRecordIndex: -1 };
this.setState({
selectedPosition: initPosition,
selectedRange: {
topLeft: initPosition,
bottomRight: initPosition,
startCell: null,
cursorCell: null,
},
});
this.props.selectNone();
};
getSelectedPosition = () => {
const { topLeft, bottomRight } = this.state.selectedRange;
return {
top: topLeft.rowIdx,
bottom: bottomRight.rowIdx,
left: topLeft.idx,
right: bottomRight.idx,
};
};
getSelectedRange = () => {
return this.state.selectedRange;
};
selectCell = (groupRecordIndex, rowIdx, idx) => {
const selectedPosition = { idx, groupRecordIndex, rowIdx };
this.setState({
selectedPosition,
selectedRange: {
topLeft: selectedPosition,
bottomRight: selectedPosition,
startCell: selectedPosition,
cursorCell: selectedPosition,
},
});
};
// onCellSelect || onKeyDown
openEditor = (event = null) => {
const { key } = event || {};
const { isEditorEnabled, selectedPosition, openEditorMode } = this.state;
const { columns } = this.props;
const selectedColumn = getSelectedColumn({ selectedPosition, columns });
// how to open editors?
// 1. editor is closed
// 2. record-cell is editable or open editor with preview mode
if (!isEditorEnabled && (this.checkIsSelectedCellEditable() || (openEditorMode === EDITOR_TYPE.PREVIEWER && checkIsColumnSupportPreview(selectedColumn)))) {
this.setState({
isEditorEnabled: true,
firstEditorKeyDown: key,
editorPosition: this.getEditorPosition()
});
}
};
closeEditor = () => {
this.setState({
isEditorEnabled: false,
firstEditorKeyDown: null,
editorPosition: null,
openEditorMode: ''
});
};
onSelectCellRangeStarted = (selectedPosition) => {
if (!this.isCellWithinBounds(selectedPosition)) return;
const selectedRange = this.createSingleCellSelectedRange(selectedPosition, true);
this.setState({ selectedRange }, () => {
if (Utils.isFunction(this.props.onCellRangeSelectionStarted)) {
this.props.onCellRangeSelectionStarted(this.state.selectedRange);
}
});
};
onSelectCellRangeUpdated = (cellPosition, isFromKeyboard, callback) => {
if (!this.state.selectedRange.isDragging && !isFromKeyboard) {
return;
}
if (!this.isCellWithinBounds(cellPosition)) {
return;
}
const startCell = this.state.selectedRange.startCell || this.state.selectedPosition;
const { topLeft, bottomRight } = getNewSelectedRange(startCell, cellPosition);
const selectedRange = {
// default the startCell to the selected cell, in case we've just started via keyboard
startCell: this.state.selectedPosition,
// assign the previous state (which will override the startCell if we already have one)
...this.state.selectedRange,
// assign the new state - the bounds of the range, and the new cursor cell
topLeft,
bottomRight,
cursorCell: cellPosition
};
this.setState({ selectedRange }, () => {
if (Utils.isFunction(this.props.onCellRangeSelectionUpdated)) {
this.props.onCellRangeSelectionUpdated(this.state.selectedRange);
}
if (Utils.isFunction(callback)) {
callback(this.state.selectedRange);
}
});
};
onSelectCellRangeEnded = () => {
const selectedRange = { ...this.state.selectedRange, isDragging: false };
this.setState({ selectedRange }, () => {
if (Utils.isFunction(this.props.onCellRangeSelectionCompleted)) {
this.props.onCellRangeSelectionCompleted(this.state.selectedRange);
}
});
};
createSingleCellSelectedRange(cellPosition, isDragging) {
return {
topLeft: cellPosition,
bottomRight: cellPosition,
startCell: cellPosition,
cursorCell: cellPosition,
isDragging
};
}
focus = () => {
if (this.selectionMask && !this.isFocused()) {
this.selectionMask.focus();
}
};
isFocused = () => {
return document.activeElement === this.selectionMask;
};
checkIsCellSelected = () => {
const { selectedPosition } = this.state;
return selectedPosition.idx !== -1 && selectedPosition.rowIdx !== -1;
};
isCellWithinBounds = ({ idx, rowIdx }) => {
const { columns, recordsCount } = this.props;
const maxRowIdx = recordsCount;
return rowIdx >= 0 && rowIdx < maxRowIdx && idx >= 0 && idx < columns.length;
};
checkIsSelectedCellEditable = () => {
const { enableCellSelect, columns, isGroupView, recordGetterByIndex, checkCanModifyRecord } = this.props;
const { selectedPosition } = this.state;
return checkIsSelectedCellEditable({ enableCellSelect, columns, isGroupView, selectedPosition, recordGetterByIndex, checkCanModifyRecord });
};
isGridSelected = () => {
return this.isCellWithinBounds(this.state.selectedPosition);
};
getSelectedDimensions = (selectedPosition) => {
const { columns, rowHeight, isGroupView, groupOffsetLeft, getRowTop: getRecordTopFromRecordsBody } = this.props;
const scrollLeft = this.props.getScrollLeft();
return {
...getSelectedDimensions({
selectedPosition, columns, scrollLeft, rowHeight, isGroupView, groupOffsetLeft, getRecordTopFromRecordsBody
})
};
};
getSelectedRangeDimensions = (selectedRange) => {
const { columns, rowHeight, isGroupView, groups, groupMetrics, groupOffsetLeft, getRowTop: getRecordTopFromRecordsBody } = this.props;
return {
...getSelectedRangeDimensions({
selectedRange, columns, rowHeight, isGroupView, groups, groupMetrics, groupOffsetLeft, getRecordTopFromRecordsBody,
})
};
};
setScrollLeft = (scrollLeft, scrollTop) => {
const { selectionMask, state: { selectedPosition } } = this;
this.setMaskScrollLeft(selectionMask, selectedPosition, scrollLeft, scrollTop);
};
geHeaderHeight = () => {
// const { table } = this.props;
const settings = {}; // table.header_settings || {};
const heightMode = isEmptyObject(settings) ? HEADER_HEIGHT_TYPE.DEFAULT : settings.header_height;
const containerHeight = heightMode === HEADER_HEIGHT_TYPE.DOUBLE ? GRID_HEADER_DOUBLE_HEIGHT : GRID_HEADER_DEFAULT_HEIGHT;
// 1: header border-bottom
return containerHeight + 1;
};
setMaskScrollLeft = (mask, position, scrollLeft, scrollTop) => {
const headerHeight = this.geHeaderHeight();
if (mask) {
const { idx, rowIdx, groupRecordIndex } = position;
if (idx >= 0 && rowIdx >= 0) {
const { columns, getRowTop, isGroupView, groupOffsetLeft } = this.props;
const column = columns[idx];
const frozen = !!column.frozen;
if (frozen) {
// use fixed
let top = -scrollTop + getRowTop(isGroupView ? groupRecordIndex : rowIdx) + headerHeight;
let left = column.left;
if (isGroupView) {
top += 1;
left += groupOffsetLeft;
}
mask.style.position = 'fixed';
mask.style.top = top + 'px';
mask.style.left = left + 'px';
mask.style.transform = 'none';
}
}
}
};
cancelSetScrollLeft = () => {
if (this.selectionMask) {
this.cancelSetMaskScrollLeft(this.selectionMask, this.state.selectedPosition);
}
};
cancelSetMaskScrollLeft = (mask, position) => {
const { left, top } = this.getSelectedDimensions(position);
mask.style.position = 'absolute';
mask.style.top = 0;
mask.style.left = 0;
mask.style.transform = `translate(${left}px, ${top}px)`;
};
getEditorPosition = () => {
if (this.selectionMask) {
const { editorPortalTarget } = this.props;
const { left: selectionMaskLeft, top: selectionMaskTop } = this.selectionMask.getBoundingClientRect();
if (editorPortalTarget === document.body) {
const { scrollLeft, scrollTop } = document.scrollingElement || document.documentElement;
return {
left: selectionMaskLeft + scrollLeft,
top: selectionMaskTop + scrollTop
};
}
const { left: portalTargetLeft, top: portalTargetTop } = editorPortalTarget.getBoundingClientRect();
const { scrollLeft, scrollTop } = editorPortalTarget;
return {
left: selectionMaskLeft - portalTargetLeft + scrollLeft,
top: selectionMaskTop - portalTargetTop + scrollTop
};
}
};
onCommit = (updated, closeEditor = true) => {
if (this.props.modifyRecord) {
this.props.modifyRecord(updated);
}
if (closeEditor) {
this.closeEditor();
}
};
onCommitCancel = () => {
this.closeEditor();
};
handleSpaceKeyDown = (e) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
const { selectedPosition } = this.state;
const { isGroupView, recordGetterByIndex } = this.props;
const record = getSelectedRow({ selectedPosition, isGroupView, recordGetterByIndex });
if (this.props.handleSpaceKeyDown) {
this.props.handleSpaceKeyDown(record);
}
};
onKeyDown = (e) => {
const keyCode = e.keyCode;
if (isCtrlKeyHeldDown(e)) {
this.onPressKeyWithCtrl(e);
} else if (keyCode === KeyCodes.Escape) {
this.onPressEscape(e);
} else if (keyCode === KeyCodes.Tab) {
this.onPressTab(e);
} else if (this.isKeyboardNavigationEvent(e)) {
this.changeCellFromEvent(e);
} else if (isSpace(e)) {
this.handleSpaceKeyDown(e);
} else if (isKeyPrintable(keyCode) || keyCode === KeyCodes.Enter) {
this.openEditor(e);
} else if (keyCode === KeyCodes.Backspace || keyCode === KeyCodes.Delete) {
const name = e.target.className;
if (name === 'rdg-selected') {
e.preventDefault();
this.handleSelectCellsDelete();
}
}
};
handleSelectCellsDelete = () => {
if (this.props.handleSelectCellsDelete) {
const { isGroupView, recordGetterByIndex, columns } = this.props;
const { selectedRange } = this.state;
const { topLeft, bottomRight } = selectedRange;
const recordsFromSelectedRange = getRecordsFromSelectedRange({ selectedRange, isGroupView, recordGetterByIndex });
if (recordsFromSelectedRange.length === 0) {
return;
}
const { idx: startColumnIdx } = topLeft;
const { idx: endColumnIdx } = bottomRight;
const columnsFromSelectedRange = columns.slice(startColumnIdx, endColumnIdx);
if (columnsFromSelectedRange.length === 0) {
return;
}
this.props.handleSelectCellsDelete(recordsFromSelectedRange, columnsFromSelectedRange);
}
};
onCopySelected = () => {
this.onCopyCells();
};
onCopy = (e) => {
e.preventDefault();
const { recordMetrics } = this.props;
// select the records to copy
const selectedRecordIds = RecordMetrics.getSelectedIds(recordMetrics);
if (selectedRecordIds.length > 0) {
this.copyRows(e, selectedRecordIds);
return;
}
// window.getSelection() doesn't work on the content of <input> in FireFox, Edge and IE.
// The selectionStart and selectionEnd properties could be used to work around this.
let selectTxt = window.getSelection().toString();
if (!selectTxt && e.target.value) {
const { selectionStart, selectionEnd } = e.target;
selectTxt = e.target.value.substring(selectionStart, selectionEnd);
}
if (selectTxt) {
this.copyText(e, selectTxt);
return;
}
// when activeElement is not cellMask, can't copy cell
if (!this.isCellMaskActive()) {
return;
}
this.onCopyCells(e);
};
onPaste = (e) => {
// when activeElement is not cellMask or has no permission, can't paste cell
if (!this.isCellMaskActive() || !this.props.canModifyRecords) return;
const { columns, isGroupView } = this.props;
const { selectedPosition, selectedRange } = this.state;
const { idx, rowIdx } = selectedPosition;
if (idx === -1 || rowIdx === -1) return; // prevent paste when no cell selected
const cliperData = getEventTransfer(e);
if (!cliperData) return;
const cliperDataType = cliperData.type;
const copied = cliperData[TRANSFER_TYPES.METADATA_FRAGMENT];
let copiedRecordsCount = 0;
let copiedColumnsCount = 0;
if (cliperDataType === TRANSFER_TYPES.METADATA_FRAGMENT) {
const { selectedRecordIds, copiedRange } = copied;
if (Array.isArray(selectedRecordIds) && selectedRecordIds.length > 0) {
// copy from selected records
copiedRecordsCount = selectedRecordIds.length;
copiedColumnsCount = columns.length;
} else {
// copy from selected range
const { topLeft: copiedTopLeft, bottomRight: copiedBottomRight } = copiedRange;
const { idx: startCopiedColumnIndex, rowIdx: startCopiedRecordIndex } = copiedTopLeft;
const { idx: endCopiedColumnIndex, rowIdx: endCopiedRecordIndex } = copiedBottomRight;
copiedRecordsCount = endCopiedRecordIndex - startCopiedRecordIndex + 1;
copiedColumnsCount = endCopiedColumnIndex - startCopiedColumnIndex + 1;
}
} else {
const { copiedRecords, copiedColumns } = copied;
copiedRecordsCount = copiedRecords.length;
copiedColumnsCount = copiedColumns.length;
}
const multiplePaste = this.isMultiplePaste(copiedRecordsCount, copiedColumnsCount);
if (this.props.paste) {
this.props.paste({
copied,
multiplePaste,
type: cliperDataType,
pasteRange: selectedRange,
columns,
isGroupView,
pasteSource: this.pasteSource,
cutPosition: this.cutPosition,
});
if (!multiplePaste) {
this.setPasteRange(copiedRecordsCount, copiedColumnsCount);
}
}
};
onCut = (event) => {
// when activeElement is not cellMask or has no permission, can't paste cell
if (!this.isCellMaskActive() || !this.props.canModifyRecords) return;
const { selectedPosition, selectedRange } = this.state;
const { idx, rowIdx } = selectedPosition;
if (idx === -1 || rowIdx === -1) return; // prevent paste when no cell selected
event.preventDefault();
const { tableId: copiedTableId, columns, isGroupView, recordGetterByIndex, getCopiedRecordsAndColumnsFromRange, getClientCellValueDisplayString } = this.props;
if (rowIdx < 0 || idx < 0) {
return; // can not copy when no cell select
}
const { topLeft, bottomRight } = selectedRange;
const copiedCellsCount = (bottomRight.rowIdx - topLeft.rowIdx + 1) * (bottomRight.idx - topLeft.idx + 1);
const type = copiedCellsCount <= 0 ? 'text' : TRANSFER_TYPES.METADATA_FRAGMENT;
const tip = copiedCellsCount > 1 ? gettext('xxx cells cut').replace('xxx', copiedCellsCount) : gettext('1 cell cut');
toaster.success(tip);
const copied = { copiedRange: selectedRange };
const { copiedRecords, copiedColumns } = getCopiedRecordsAndColumnsFromRange({ type, copied, columns, isGroupView });
setEventTransfer({
type,
event,
copiedRange: { ...selectedRange },
copiedRecords,
copiedColumns,
copiedTableId,
tableData: {
columns,
},
isGroupView,
recordGetterByIndex,
getClientCellValueDisplayString,
});
if (copiedCellsCount > 0) {
this.pasteSource = PASTE_SOURCE.CUT;
this.cutPosition = { ...selectedPosition };
}
};
copyText = (event, copiedText) => {
const type = 'text';
setEventTransfer({
type,
event,
copiedText,
});
};
copyRows = (event, selectedRecordIds) => {
const { tableId: copiedTableId, columns, recordGetterById, isGroupView, getCopiedRecordsAndColumnsFromRange, getClientCellValueDisplayString } = this.props;
const copiedRowsCount = selectedRecordIds.length;
toaster.success(
copiedRowsCount > 1 ? gettext('xxx rows are copied.').replace('xxx', copiedRowsCount) : gettext('1 row is copied.')
);
const type = TRANSFER_TYPES.METADATA_FRAGMENT;
const copied = { selectedRecordIds };
const { copiedRecords, copiedColumns } = getCopiedRecordsAndColumnsFromRange({ type, copied, columns, isGroupView });
setEventTransfer({
type,
event,
selectedRecordIds,
copiedRecords,
copiedColumns,
copiedTableId,
tableData: {
columns,
},
recordGetterById,
getClientCellValueDisplayString,
});
};
onCopyCells = (event) => {
const { tableId: copiedTableId, columns, isGroupView, recordGetterByIndex, getCopiedRecordsAndColumnsFromRange, getClientCellValueDisplayString } = this.props;
const { selectedPosition, selectedRange } = this.state;
const { rowIdx, idx } = selectedPosition;
if (rowIdx < 0 || idx < 0) {
return; // can not copy when no cell select
}
const { topLeft, bottomRight } = selectedRange;
const type = TRANSFER_TYPES.METADATA_FRAGMENT;
const copiedCellsCount = (bottomRight.rowIdx - topLeft.rowIdx + 1) * (bottomRight.idx - topLeft.idx + 1);
toaster.success(
copiedCellsCount > 1 ? gettext('xxx cells copied').replace('xxx', copiedCellsCount) : gettext('1 cell copied')
);
const copied = { copiedRange: selectedRange };
const { copiedRecords, copiedColumns } = getCopiedRecordsAndColumnsFromRange({ type, copied, columns, isGroupView });
setEventTransfer({
type,
event,
copiedRange: { ...selectedRange },
copiedRecords,
copiedColumns,
copiedTableId,
tableData: {
columns,
},
isGroupView,
recordGetterByIndex,
getClientCellValueDisplayString,
});
this.cutPosition = null;
this.pasteSource = PASTE_SOURCE.COPY;
};
isMultiplePaste = (copiedRecordsCount, copiedColumnsCount) => {
const { selectedRange } = this.state;
const { topLeft, bottomRight } = selectedRange;
const { idx: startColumnIndex, rowIdx: startRecordIndex } = topLeft;
const { idx: endColumnIndex, rowIdx: endRecordIndex } = bottomRight;
return Number.isInteger((endColumnIndex - startColumnIndex + 1) / copiedColumnsCount) && Number.isInteger((endRecordIndex - startRecordIndex + 1) / copiedRecordsCount);
};
setPasteRange = (copiedRecordsCount, copiedColumnsCount) => {
const { recordsCount, columns } = this.props;
const { selectedPosition, selectedRange } = this.state;
const { topLeft } = selectedRange;
const { idx, rowIdx } = topLeft;
const columnsLen = columns.length;
const groupRecordIndex = selectedPosition.groupRecordIndex;
let nextColumnIndex = idx + copiedColumnsCount - 1;
let nextRecordIndex = rowIdx + copiedRecordsCount - 1;
if (nextColumnIndex >= columnsLen) {
nextColumnIndex = columnsLen - 1;
}
if (nextRecordIndex >= recordsCount) {
nextRecordIndex = recordsCount - 1;
}
const nextSelectedRange = {
topLeft,
startCell: selectedPosition,
bottomRight: {
idx: nextColumnIndex,
rowIdx: nextRecordIndex,
groupRecordIndex,
},
cursorCell: {
idx: selectedPosition.idx,
rowIdx: selectedPosition.rowIdx,
groupRecordIndex,
}
};
this.setState({
selectedRange: {
...selectedRange,
...nextSelectedRange
}
}, () => {
this.focus();
});
return nextSelectedRange;
};
onPressKeyWithCtrl = () => {
};
onPressEscape = () => {
};
onPressTab = (e) => {
this.changeCellFromEvent(e);
};
getLeftInterval = () => {
const { isGroupView, columns, groupOffsetLeft, frozenColumnsWidth } = this.props;
const firstColumnFrozen = columns[0] ? columns[0].frozen : false;
let leftInterval = 0;
if (firstColumnFrozen) {
leftInterval = groupOffsetLeft + frozenColumnsWidth;
if (isGroupView) {
leftInterval += groupOffsetLeft;
}
} else {
leftInterval = 0;
}
return leftInterval;
};
handleVerticalArrowAction = (current, actionType) => {
const { isGroupView, groupMetrics, rowHeight } = this.props;
const step = actionType === 'ArrowDown' ? 1 : -1;
if (isGroupView) {
const groupRows = groupMetrics.groupRows || [];
const groupRowsLen = groupRows.length;
const { groupRecordIndex: currentGroupRowIndex } = current;
let nextGroupRowIndex = currentGroupRowIndex + step;
let nextGroupRow;
while (nextGroupRowIndex > 0 && nextGroupRowIndex < groupRowsLen) {
nextGroupRow = getGroupRecordByIndex(nextGroupRowIndex, groupMetrics);
if (nextGroupRow.type === GROUP_ROW_TYPE.ROW) {
break;
}
nextGroupRowIndex += step;
}
if (!nextGroupRow || nextGroupRow.type !== GROUP_ROW_TYPE.ROW) {
return;
}
const currentScrollTop = this.props.getGroupCanvasScrollTop() || 0;
const { rowIdx: nextRowIdx, top: nextRowTop } = nextGroupRow;
let newScrollTop;
// 32: footerHeight; 16: preview of next row.
const HEADER_HEIGHT = 150;
if (nextRowTop <= currentScrollTop + 16) {
newScrollTop = nextRowTop - 16;
} else if (nextRowTop + HEADER_HEIGHT - currentScrollTop >= window.innerHeight - 32 - 16) {
newScrollTop = nextRowTop + HEADER_HEIGHT - window.innerHeight + 32 + rowHeight + 16;
}
if (newScrollTop !== undefined) {
this.props.setGroupCanvasScrollTop(newScrollTop);
}
return { ...current, rowIdx: nextRowIdx, groupRecordIndex: nextGroupRowIndex };
} else {
return { ...current, rowIdx: current.rowIdx + step };
}
};
handleLeftArrowAction = (current) => {
let cellContainer = this.selectionMask;
if (!cellContainer) return;
const { columns } = this.props;
const rect = cellContainer.getBoundingClientRect();
const leftInterval = this.getLeftInterval();
const nextColumnWidth = columns[current.idx - 1] ? columns[current.idx - 1].width : 0;
const { left: tableContentLeft, right } = this.props.getTableContentRect();
const viewLeft = tableContentLeft + 130;
// selectMask is outside the viewport, scroll to next column
if (rect.x < 0 || rect.x > right) {
this.props.scrollToColumn(current.idx - 1);
} else if (nextColumnWidth > rect.x - leftInterval - viewLeft) {
// selectMask is part of the viewport, newScrollLeft = columnWidth - visibleWidth
const newScrollLeft = nextColumnWidth - (rect.x - leftInterval - viewLeft);
this.props.setRecordsScrollLeft(this.props.getScrollLeft() - newScrollLeft);
}
return ({ ...current, idx: current.idx === 0 ? 0 : current.idx - 1 });
};
handleRightArrowAction = (current) => {
let cellContainer = this.selectionMask;
if (!cellContainer) return;
const { columns } = this.props;
const rect = cellContainer.getBoundingClientRect();
const columnIdx = current.idx;
const column = columns[columnIdx];
if (columnIdx === 1 && column.frozen === true) {
this.props.scrollToColumn(1);
} else {
const { right: tableContentRight } = this.props.getTableContentRect();
const nextColumnWidth = columns[columnIdx + 1] ? columns[columnIdx + 1].width : 0;
// selectMask is outside the viewport, scroll to next column
if (rect.x < 0 || rect.x > tableContentRight) {
this.props.scrollToColumn(columnIdx + 1);
} else if (rect.x + rect.width + nextColumnWidth > tableContentRight) {
// selectMask is part of the viewport, newScrollLeft = columnWidth - visibleWidth
const newScrollLeft = nextColumnWidth - (tableContentRight - rect.x - rect.width);
this.props.setRecordsScrollLeft(this.props.getScrollLeft() + newScrollLeft);
}
}
return ({ ...current, idx: current.idx + 1 });
};
isKeyboardNavigationEvent(e) {
return this.getKeyNavActionFromEvent(e) != null;
}
getKeyNavActionFromEvent = (e) => {
const { getVisibleIndex, onHitBottomBoundary, onHitTopBoundary } = this.props;
const { rowVisibleStartIdx, rowVisibleEndIdx } = getVisibleIndex();
const isCellAtBottomBoundary = cell => cell.rowIdx >= rowVisibleEndIdx - 1;
const isCellAtTopBoundary = cell => cell.rowIdx !== 0 && cell.rowIdx <= rowVisibleStartIdx;
const keyNavActions = {
ArrowDown: {
getNext: (current) => {
return this.handleVerticalArrowAction(current, 'ArrowDown');
},
isCellAtBoundary: isCellAtBottomBoundary,
onHitBoundary: onHitBottomBoundary
},
ArrowUp: {
getNext: (current) => {
return this.handleVerticalArrowAction(current, 'ArrowUp');
},
isCellAtBoundary: isCellAtTopBoundary,
onHitBoundary: onHitTopBoundary
},
ArrowRight: {
getNext: (current) => {
return this.handleRightArrowAction(current);
},
isCellAtBoundary: () => {
return false;
}
},
ArrowLeft: {
getNext: (current) => {
return this.handleLeftArrowAction(current);
},
isCellAtBoundary: () => {
return false;
}
}
};
if (e.keyCode === KeyCodes.Tab) {
return e.shiftKey === true ? keyNavActions.ArrowLeft : keyNavActions.ArrowRight;
}
return keyNavActions[e.key];
};
changeCellFromEvent = (e) => {
e.preventDefault();
if (e.keyCode === KeyCodes.ChineseInputMethod && this.state.isEditorEnabled) {
return;
}
if (this.throttle) return;
const currentPosition = this.state.selectedPosition;
const keyNavAction = this.getKeyNavActionFromEvent(e);
const next = keyNavAction.getNext(currentPosition);
if (!next) return;
this.checkIsAtGridBoundary(keyNavAction, next);
this.props.onCellClick(next);
this.onSelectCell({ ...next });
this.throttle = true;
setTimeout(() => {
this.throttle = false;
}, 30);
};
checkIsAtGridBoundary(keyNavAction, next) {
const { isCellAtBoundary, onHitBoundary } = keyNavAction;
if (isCellAtBoundary(next)) {
onHitBoundary(next);
}
}
onFocus = () => {
};
onScroll = (e) => {
e.stopPropagation();
};
setSelectionMaskRef = (ref) => {
this.selectionMask = ref;
};
setSelectionRangeMaskRef = (ref) => {
this.selectedRangeMask = ref;
};
setContainerRef = (ref) => {
this.container = ref;
};
isCellMaskActive = () => {
const activeElement = document.activeElement;
return (activeElement &&
(activeElement.getAttribute('data-test') === 'cell-mask' ||
activeElement.getAttribute('data-test') === 'active-editor')
);
};
handleDragCopy = (draggedRange) => {
const { columns, groupMetrics } = this.props;
// compute the new records
const newRecords = this.props.getUpdateDraggedRecords(draggedRange, columns, groupMetrics);
if (this.props.modifyRecords) {
this.props.modifyRecords({ ...newRecords, isCopyPaste: true });
}
};
handleDragStart = (e) => {
const { selectedRange: { topLeft, bottomRight, startCell, cursorCell } } = this.state;
// To prevent dragging down/up when reordering rows. (TODO: is this required)
const isViewportDragging = e && e.target && e.target.className;
if (topLeft.idx > -1 && isViewportDragging) {
try {
e.dataTransfer.setData('text/plain', '');
} catch (ex) {
// IE only supports 'text' and 'URL' for the 'type' argument
e.dataTransfer.setData('text', '');
}
this.setState({
draggedRange: { topLeft, bottomRight, startCell, cursorCell }
});
}
};
handleDragEnter = ({ overRecordIdx, overGroupRecordIndex }) => {
if (this.state.draggedRange != null) {
this.setState(({ draggedRange }) => ({
draggedRange: { ...draggedRange, overRecordIdx, overGroupRecordIndex }
}));
}
};
handleDragEnd = () => {
const { draggedRange, selectedRange } = this.state;
let newSelectedRange = deepCopy(selectedRange);
if (draggedRange !== null) {
const { overRecordIdx, overGroupRecordIndex, bottomRight } = draggedRange;
if (overRecordIdx !== null && bottomRight.rowIdx < overRecordIdx) {
this.handleDragCopy(draggedRange);
newSelectedRange.bottomRight.rowIdx = overRecordIdx;
newSelectedRange.cursorCell.rowIdx = overRecordIdx;
newSelectedRange.bottomRight.groupRecordIndex = overGroupRecordIndex;
newSelectedRange.cursorCell.groupRecordIndex = overGroupRecordIndex;
}
this.setState({ draggedRange: null, selectedRange: newSelectedRange });
}
};
renderSingleCellSelectView = () => {
const { isEditorEnabled, selectedPosition } = this.state;
const isDragEnabled = this.checkIsSelectedCellEditable();
const showDragHandle = (isDragEnabled && this.props.canModifyRecords);
if (isEditorEnabled) {
return null;
}
if (!this.isGridSelected()) return null;
const props = {
innerRef: this.setSelectionMaskRef,
selectedPosition,
getSelectedDimensions: this.getSelectedDimensions,
};
return (
<SelectionMask {...props}>
{showDragHandle ? <DragHandler onDragStart={this.handleDragStart} onDragEnd={this.handleDragEnd} /> : null}
</SelectionMask>
);
};
renderCellRangeSelectView = () => {
const { selectedRange } = this.state;
const { columns, rowHeight } = this.props;
const isDragEnabled = this.checkIsSelectedCellEditable();
const showDragHandle = (isDragEnabled && this.props.canModifyRecords);
return [
<SelectionRangeMask
key="range-mask"
innerRef={this.setSelectionRangeMaskRef}
selectedRange={selectedRange}
columns={columns}
rowHeight={rowHeight}
getSelectedRangeDimensions={this.getSelectedRangeDimensions}
>
{showDragHandle ? <DragHandler onDragStart={this.handleDragStart} onDragEnd={this.handleDragEnd} /> : null}
</SelectionRangeMask>,
<SelectionMask
key="selection-mask"
innerRef={this.setSelectionMaskRef}
selectedPosition={selectedRange.startCell}
getSelectedDimensions={this.getSelectedDimensions}
/>
];
};
render() {
const { selectedRange, isEditorEnabled, draggedRange, selectedPosition, firstEditorKeyDown, openEditorMode, editorPosition } = this.state;
const { columns, isGroupView, recordGetterByIndex, scrollTop, getScrollLeft, editorPortalTarget, contextMenu } = this.props;
const isSelectedSingleCell = selectedRangeIsSingleCell(selectedRange);
return (
<div
className='interaction-mask'
ref={this.setContainerRef}
onKeyDown={this.onKeyDown}
onFocus={this.onFocus}
onScroll={this.onScroll}
>
{draggedRange && (
<DragMask
draggedRange={draggedRange}
getSelectedDimensions={this.getSelectedDimensions}
getSelectedRangeDimensions={this.getSelectedRangeDimensions}
/>
)}
{isSelectedSingleCell ? this.renderSingleCellSelectView() : this.renderCellRangeSelectView()}
{isEditorEnabled && (
<EditorPortal target={editorPortalTarget}>
<EditorContainer
columns={columns}
scrollTop={scrollTop}
firstEditorKeyDown={firstEditorKeyDown}
openEditorMode={openEditorMode}
portalTarget={editorPortalTarget}
scrollLeft={getScrollLeft()}
record={getSelectedRow({ selectedPosition, isGroupView, recordGetterByIndex })}
column={getSelectedColumn({ selectedPosition, columns })}
value={getSelectedCellValue({ selectedPosition, columns, isGroupView, recordGetterByIndex })}
editorPosition={editorPosition}
onCommit={this.onCommit}
onCommitCancel={this.onCommitCancel}
modifyColumnData={this.props.modifyColumnData}
{...{
...this.getSelectedDimensions(selectedPosition),
...this.state.editorPosition
}}
/>
</EditorPortal>
)}
{isValidElement(contextMenu) && cloneElement(contextMenu, {
selectedPosition: isSelectedSingleCell ? selectedPosition : null,
selectedRange: !isSelectedSingleCell ? selectedRange : null,
onClearSelected: this.handleSelectCellsDelete,
onCopySelected: this.onCopySelected,
getTableContentRect: this.props.getTableContentRect,
getTableCanvasContainerRect: this.props.getTableCanvasContainerRect,
})}
</div>
);
}
}
InteractionMasks.propTypes = {
contextmenu: PropTypes.element,
tableId: PropTypes.string,
columns: PropTypes.array,
canAddRow: PropTypes.bool,
isGroupView: PropTypes.bool,
recordsCount: PropTypes.number,
recordMetrics: PropTypes.object,
groups: PropTypes.array,
groupMetrics: PropTypes.object,
rowHeight: PropTypes.number,
groupOffsetLeft: PropTypes.number,
frozenColumnsWidth: PropTypes.number,
enableCellSelect: PropTypes.bool,
canModifyRecords: PropTypes.bool,
getRowTop: PropTypes.func,
scrollTop: PropTypes.number,
getScrollLeft: PropTypes.func,
getTableContentRect: PropTypes.func,
getMobileFloatIconStyle: PropTypes.func,
onToggleMobileMoreOperations: PropTypes.func,
onToggleInsertRecordDialog: PropTypes.func,
onCellRangeSelectionStarted: PropTypes.func,
onCellRangeSelectionUpdated: PropTypes.func,
onCellRangeSelectionCompleted: PropTypes.func,
selectNone: PropTypes.func,
checkCanModifyRecord: PropTypes.func,
editorPortalTarget: PropTypes.instanceOf(Element).isRequired,
modifyRecord: PropTypes.func,
modifyColumnData: PropTypes.func,
recordGetterByIndex: PropTypes.func,
recordGetterById: PropTypes.func,
modifyRecords: PropTypes.func,
deleteRecordsLinks: PropTypes.func,
paste: PropTypes.func,
editMobileCell: PropTypes.func,
getVisibleIndex: PropTypes.func,
onHitBottomBoundary: PropTypes.func,
onHitTopBoundary: PropTypes.func,
onCellClick: PropTypes.func,
scrollToColumn: PropTypes.func,
setRecordsScrollLeft: PropTypes.func,
getGroupCanvasScrollTop: PropTypes.func,
setGroupCanvasScrollTop: PropTypes.func,
appPage: PropTypes.object,
onFillingDragRows: PropTypes.func,
onCellsDragged: PropTypes.func,
getUpdateDraggedRecords: PropTypes.func,
getCopiedRecordsAndColumnsFromRange: PropTypes.func,
onCommit: PropTypes.func,
getTableCanvasContainerRect: PropTypes.func,
handleSpaceKeyDown: PropTypes.func,
};
export default InteractionMasks;