mirror of
https://github.com/haiwen/seahub.git
synced 2025-08-31 22:54:11 +00:00
feat(tag): display tags with table (#7311)
This commit is contained in:
18
frontend/src/components/sf-table/constants/event-bus-type.js
Normal file
18
frontend/src/components/sf-table/constants/event-bus-type.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export const EVENT_BUS_TYPE = {
|
||||
OPEN_EDITOR: 'open_editor',
|
||||
CLOSE_EDITOR: 'close_editor',
|
||||
SELECT_CELL: 'select_cell',
|
||||
SELECT_START: 'select_start',
|
||||
SELECT_UPDATE: 'select_update',
|
||||
SELECT_END: 'select_end',
|
||||
SELECT_END_WITH_SHIFT: 'select_end_with_shift',
|
||||
SELECT_NONE: 'select_none',
|
||||
COPY_CELLS: 'copy_cells',
|
||||
PASTE_CELLS: 'paste_cells',
|
||||
CUT_CELLS: 'cut_cells',
|
||||
SELECT_COLUMN: 'select_column',
|
||||
DRAG_ENTER: 'drag_enter',
|
||||
COLLAPSE_ALL_GROUPS: 'collapse_all_groups',
|
||||
EXPAND_ALL_GROUPS: 'expand_all_groups',
|
||||
FOCUS_CANVAS: 'focus_canvas',
|
||||
};
|
28
frontend/src/components/sf-table/constants/grid.js
Normal file
28
frontend/src/components/sf-table/constants/grid.js
Normal file
@@ -0,0 +1,28 @@
|
||||
export const CANVAS_RIGHT_INTERVAL = 44;
|
||||
|
||||
export const OVER_SCAN_COLUMNS = 10;
|
||||
|
||||
export const HEADER_HEIGHT_TYPE = {
|
||||
DEFAULT: 'default',
|
||||
DOUBLE: 'double',
|
||||
};
|
||||
|
||||
export const SEQUENCE_COLUMN_WIDTH = 80;
|
||||
|
||||
export const GRID_HEADER_DEFAULT_HEIGHT = 32;
|
||||
|
||||
export const GRID_HEADER_DOUBLE_HEIGHT = 56;
|
||||
|
||||
export const INSERT_ROW_HEIGHT = 32;
|
||||
|
||||
export const MIN_COLUMN_WIDTH = 50;
|
||||
|
||||
export const EDITOR_TYPE = {
|
||||
PREVIEWER: 'previewer',
|
||||
ADDITION: 'addition',
|
||||
};
|
||||
|
||||
export const PASTE_SOURCE = {
|
||||
COPY: 'copy',
|
||||
CUT: 'cut',
|
||||
};
|
9
frontend/src/components/sf-table/constants/group.js
Normal file
9
frontend/src/components/sf-table/constants/group.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export const GROUP_VIEW_OFFSET = 16;
|
||||
|
||||
export const GROUP_ROW_TYPE = {
|
||||
GROUP_CONTAINER: 'group_container',
|
||||
ROW: 'row',
|
||||
BTN_INSERT_ROW: 'btn_insert_row',
|
||||
};
|
||||
|
||||
export const GROUP_HEADER_HEIGHT = 48;
|
15
frontend/src/components/sf-table/constants/transfer-types.js
Normal file
15
frontend/src/components/sf-table/constants/transfer-types.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const FRAGMENT = 'application/x-sf-metadata-fragment';
|
||||
const HTML = 'text/html';
|
||||
const TEXT = 'text/plain';
|
||||
const FILES = 'files';
|
||||
const METADATA_FRAGMENT = 'sf-metadata-fragment';
|
||||
|
||||
const TRANSFER_TYPES = {
|
||||
FRAGMENT,
|
||||
HTML,
|
||||
TEXT,
|
||||
FILES,
|
||||
METADATA_FRAGMENT,
|
||||
};
|
||||
|
||||
export default TRANSFER_TYPES;
|
23
frontend/src/components/sf-table/constants/z-index.js
Normal file
23
frontend/src/components/sf-table/constants/z-index.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// CellMasks should render in front of the cells
|
||||
// Unfrozen cells do not have a zIndex specifed
|
||||
export const CELL_MASK = 1;
|
||||
export const SEQUENCE_COLUMN = 2;
|
||||
|
||||
// higher than unfrozen header cell(0), RESIZE_HANDLE
|
||||
export const FROZEN_HEADER_CELL = 2;
|
||||
export const GROUP_FROZEN_HEADER = 2;
|
||||
export const SCROLL_BAR = 2;
|
||||
|
||||
// In front of CELL_MASK/non-frozen cell(1)、back of the frozen cells (2)
|
||||
export const GROUP_BACKDROP = 2;
|
||||
export const FROZEN_GROUP_CELL = 2;
|
||||
|
||||
// Frozen cells have a zIndex value of 2 so CELL_MASK should have a higher value
|
||||
export const FROZEN_CELL_MASK = 3;
|
||||
|
||||
// need higher than the doms(etc. cell, cell_mask) which behind of the grid header
|
||||
export const GRID_HEADER = 4;
|
||||
export const GRID_FOOTER = 4;
|
||||
|
||||
// EditorContainer is rendered outside the grid and it higher FROZEN_GROUP_CELL(2)
|
||||
export const EDITOR_CONTAINER = 9;
|
6
frontend/src/components/sf-table/context-menu/index.css
Normal file
6
frontend/src/components/sf-table/context-menu/index.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.sf-table-context-menu {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
box-shadow: 0 0 5px #ccc;
|
||||
position: fixed;
|
||||
}
|
109
frontend/src/components/sf-table/context-menu/index.js
Normal file
109
frontend/src/components/sf-table/context-menu/index.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const ContextMenu = ({
|
||||
createContextMenuOptions, getTableContentRect, getTableCanvasContainerRect, ...customProps
|
||||
}) => {
|
||||
const menuRef = useRef(null);
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||
|
||||
const handleHide = useCallback((event) => {
|
||||
if (!menuRef.current && visible) {
|
||||
setVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||
setVisible(false);
|
||||
}
|
||||
}, [menuRef, visible]);
|
||||
|
||||
const getMenuPosition = useCallback((x = 0, y = 0) => {
|
||||
let menuStyles = {
|
||||
top: y,
|
||||
left: x
|
||||
};
|
||||
if (!menuRef.current) return menuStyles;
|
||||
const rect = menuRef.current.getBoundingClientRect();
|
||||
const tableCanvasContainerRect = getTableCanvasContainerRect();
|
||||
const tableContentRect = getTableContentRect();
|
||||
const { right: innerWidth, bottom: innerHeight } = tableContentRect;
|
||||
menuStyles.top = menuStyles.top - tableCanvasContainerRect.top;
|
||||
menuStyles.left = menuStyles.left - tableCanvasContainerRect.left;
|
||||
|
||||
if (y + rect.height > innerHeight - 10) {
|
||||
menuStyles.top -= rect.height;
|
||||
}
|
||||
if (x + rect.width > innerWidth) {
|
||||
menuStyles.left -= rect.width;
|
||||
}
|
||||
if (menuStyles.top < 0) {
|
||||
menuStyles.top = rect.bottom > innerHeight ? (innerHeight - 10 - rect.height) / 2 : 0;
|
||||
}
|
||||
if (menuStyles.left < 0) {
|
||||
menuStyles.left = rect.width < innerWidth ? (innerWidth - rect.width) / 2 : 0;
|
||||
}
|
||||
return menuStyles;
|
||||
}, [getTableContentRect, getTableCanvasContainerRect]);
|
||||
|
||||
const handleShow = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
if (menuRef.current && menuRef.current.contains(event.target)) return;
|
||||
|
||||
setVisible(true);
|
||||
|
||||
const position = getMenuPosition(event.clientX, event.clientY);
|
||||
setPosition(position);
|
||||
}, [getMenuPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('contextmenu', handleShow);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('contextmenu', handleShow);
|
||||
};
|
||||
|
||||
}, [handleShow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
document.addEventListener('mousedown', handleHide);
|
||||
} else {
|
||||
document.removeEventListener('mousedown', handleHide);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleHide);
|
||||
};
|
||||
}, [visible, handleHide]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (!visible || !createContextMenuOptions) return [];
|
||||
return createContextMenuOptions({ ...customProps, hideMenu: setVisible });
|
||||
}, [customProps, visible, createContextMenuOptions]);
|
||||
|
||||
|
||||
if (!Array.isArray(options) || options.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className='dropdown-menu sf-table-context-menu'
|
||||
style={position}
|
||||
>
|
||||
{options}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ContextMenu.propTypes = {
|
||||
createContextMenuOptions: PropTypes.func,
|
||||
getTableContentRect: PropTypes.func,
|
||||
getTableCanvasContainerRect: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ContextMenu;
|
@@ -0,0 +1,11 @@
|
||||
import { cloneElement, forwardRef, isValidElement } from 'react';
|
||||
|
||||
const Editor = forwardRef(({ column, editorProps }, ref) => {
|
||||
|
||||
if (!isValidElement(column.editor)) {
|
||||
return null;
|
||||
}
|
||||
return cloneElement(column.editor, { ...editorProps, ref });
|
||||
});
|
||||
|
||||
export default Editor;
|
@@ -0,0 +1,27 @@
|
||||
import React, { isValidElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import NormalEditorContainer from './normal-editor-container';
|
||||
import PopupEditorContainer from './popup-editor-container';
|
||||
import PreviewEditorContainer from './preview-editor-container';
|
||||
import { checkIsColumnSupportPreview, checkIsPopupColumnEditor } from '../../utils/column';
|
||||
import { EDITOR_TYPE } from '../../constants/grid';
|
||||
|
||||
const EditorContainer = (props) => {
|
||||
const { column, openEditorMode } = props;
|
||||
if (!column || !isValidElement(column.editor)) return null;
|
||||
|
||||
if (checkIsPopupColumnEditor(column)) {
|
||||
return <PopupEditorContainer { ...props } />;
|
||||
}
|
||||
if (checkIsColumnSupportPreview(column) && openEditorMode === EDITOR_TYPE.PREVIEWER) {
|
||||
return <PreviewEditorContainer { ...props } />;
|
||||
}
|
||||
return <NormalEditorContainer { ...props } />;
|
||||
};
|
||||
|
||||
EditorContainer.propTypes = {
|
||||
column: PropTypes.object,
|
||||
openEditorMode: PropTypes.string,
|
||||
};
|
||||
|
||||
export default EditorContainer;
|
@@ -0,0 +1,365 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { ClickOutside } from '@seafile/sf-metadata-ui-component';
|
||||
import Editor from './editor';
|
||||
import { EDITOR_CONTAINER as Z_INDEX_EDITOR_CONTAINER } from '../../constants/z-index';
|
||||
import { Utils } from '../../../../utils/utils';
|
||||
import { getEventClassName } from '../../utils';
|
||||
import { checkCellValueChanged } from '../../utils/cell-comparer';
|
||||
import { getCellValueByColumn } from '../../utils/cell';
|
||||
import { isCtrlKeyHeldDown, isKeyPrintable } from '../../../../utils/keyboard-utils';
|
||||
import EventBus from '../../../common/event-bus';
|
||||
import { EVENT_BUS_TYPE } from '../../constants/event-bus-type';
|
||||
import { checkIsPrivateColumn } from '../../utils/column';
|
||||
|
||||
class NormalEditorContainer extends React.Component {
|
||||
|
||||
static displayName = 'EditorContainer';
|
||||
|
||||
state = { isInvalid: false };
|
||||
changeCommitted = false;
|
||||
changeCanceled = false;
|
||||
eventBus = EventBus.getInstance();
|
||||
|
||||
componentDidMount() {
|
||||
const inputNode = this.getInputNode();
|
||||
if (inputNode !== undefined) {
|
||||
this.setTextInputFocus();
|
||||
if (this.getEditor() && !this.getEditor().disableContainerStyles) {
|
||||
inputNode.className += ' sf-metadata-editor-main';
|
||||
inputNode.style.height = (this.props.height - 1) + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.scrollLeft !== this.props.scrollLeft || prevProps.scrollTop !== this.props.scrollTop) {
|
||||
this.commitCancel();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (!this.changeCommitted && !this.changeCanceled) {
|
||||
this.commit();
|
||||
}
|
||||
}
|
||||
|
||||
isKeyExplicitlyHandled = (key) => {
|
||||
return Utils.isFunction(this['onPress' + key]);
|
||||
};
|
||||
|
||||
checkAndCall = (methodName, args) => {
|
||||
if (Utils.isFunction(this[methodName])) {
|
||||
this[methodName](args);
|
||||
}
|
||||
};
|
||||
|
||||
onKeyDown = (e) => {
|
||||
if (isCtrlKeyHeldDown(e)) {
|
||||
this.checkAndCall('onPressKeyWithCtrl', e);
|
||||
} else if (this.isKeyExplicitlyHandled(e.key)) {
|
||||
// break up individual keyPress events to have their own specific callbacks
|
||||
const callBack = 'onPress' + e.key;
|
||||
this.checkAndCall(callBack, e);
|
||||
} else if (isKeyPrintable(e.keyCode)) {
|
||||
e.stopPropagation();
|
||||
this.checkAndCall('onPressChar', e);
|
||||
}
|
||||
|
||||
// Track which keys are currently down for shift clicking etc
|
||||
this._keysDown = this._keysDown || {};
|
||||
this._keysDown[e.keyCode] = true;
|
||||
if (Utils.isFunction(this.props.onGridKeyDown)) {
|
||||
this.props.onGridKeyDown(e);
|
||||
}
|
||||
};
|
||||
|
||||
onScroll = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
setEditorRef = (editor) => {
|
||||
this.editor = editor;
|
||||
};
|
||||
|
||||
createEditor = () => {
|
||||
const { column, openEditorMode, columns, modifyColumnData, readOnly } = this.props;
|
||||
const editorProps = {
|
||||
ref: this.setEditorRef,
|
||||
readOnly,
|
||||
columns,
|
||||
column,
|
||||
value: this.getInitialValue(),
|
||||
mode: openEditorMode,
|
||||
onCommit: this.commit,
|
||||
onCommitData: this.commitData,
|
||||
onCommitCancel: this.commitCancel,
|
||||
recordMetaData: this.getRecordMetaData(),
|
||||
record: this.props.record,
|
||||
height: this.props.height,
|
||||
onBlur: this.commit,
|
||||
onOverrideKeyDown: this.onKeyDown,
|
||||
modifyColumnData,
|
||||
};
|
||||
|
||||
return (
|
||||
<Editor column={column} editorProps={editorProps} />
|
||||
);
|
||||
};
|
||||
|
||||
onPressEnter = () => {
|
||||
// this.commit({ key: 'Enter' });
|
||||
};
|
||||
|
||||
onPressTab = () => {
|
||||
this.commit({ key: 'Tab' });
|
||||
};
|
||||
|
||||
onPressEscape = (e) => {
|
||||
if (!this.editorIsSelectOpen()) {
|
||||
this.commitCancel();
|
||||
} else {
|
||||
// prevent event from bubbling if editor has results to select
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
onPressArrowDown = (e) => {
|
||||
if (this.editorHasResults()) {
|
||||
// don't want to propagate as that then moves us round the grid
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
this.commit(e);
|
||||
}
|
||||
};
|
||||
|
||||
onPressArrowUp = (e) => {
|
||||
if (this.editorHasResults()) {
|
||||
// don't want to propagate as that then moves us round the grid
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
this.commit(e);
|
||||
}
|
||||
};
|
||||
|
||||
onPressArrowLeft = (e) => {
|
||||
// prevent event propagation. this disables left cell navigate
|
||||
if (!this.isCaretAtBeginningOfInput()) {
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
this.commit(e);
|
||||
}
|
||||
};
|
||||
|
||||
onPressArrowRight = (e) => {
|
||||
// prevent event propagation. this disables right cell navigate
|
||||
if (!this.isCaretAtEndOfInput()) {
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
this.commit(e);
|
||||
}
|
||||
};
|
||||
|
||||
editorHasResults = () => {
|
||||
if (this.getEditor() && Utils.isFunction(this.getEditor().hasResults)) {
|
||||
return this.getEditor().hasResults();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
editorIsSelectOpen = () => {
|
||||
if (this.getEditor() && Utils.isFunction(this.getEditor().isSelectOpen)) {
|
||||
return this.getEditor().isSelectOpen();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
getRecordMetaData = () => {
|
||||
// clone row data so editor cannot actually change this
|
||||
// convention based method to get corresponding Id or Name of any Name or Id property
|
||||
if (typeof this.props.column.getRecordMetaData === 'function') {
|
||||
return this.props.column.getRecordMetaData(this.props.record, this.props.column);
|
||||
}
|
||||
};
|
||||
|
||||
getEditor = () => {
|
||||
return this.editor;
|
||||
};
|
||||
|
||||
getInputNode = () => {
|
||||
if (this.getEditor() && this.getEditor().getInputNode) {
|
||||
return this.getEditor().getInputNode();
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
getInitialValue = () => {
|
||||
const { firstEditorKeyDown: key, value } = this.props;
|
||||
if (key === 'Enter') {
|
||||
return value;
|
||||
}
|
||||
return key || value;
|
||||
};
|
||||
|
||||
getContainerClass = () => {
|
||||
return classnames({
|
||||
'rdg-editor-container': true,
|
||||
'table-cell-editor': true,
|
||||
'has-error': this.state.isInvalid === true
|
||||
});
|
||||
};
|
||||
|
||||
getOldRowData = (originalOldCellValue) => {
|
||||
const { column } = this.props;
|
||||
const { key: columnKey, name: columnName } = column;
|
||||
let oldValue = originalOldCellValue;
|
||||
if (this.getEditor() && this.getEditor().getOldValue) {
|
||||
const original = this.getEditor().getOldValue();
|
||||
oldValue = original[Object.keys(original)[0]];
|
||||
}
|
||||
const oldRowData = checkIsPrivateColumn(column) ? { [columnName]: oldValue } : { [columnName]: oldValue };
|
||||
const originalOldRowData = { [columnKey]: originalOldCellValue }; // { [column.key]: cellValue }
|
||||
return { oldRowData, originalOldRowData };
|
||||
};
|
||||
|
||||
commit = (args) => {
|
||||
const { record, column } = this.props;
|
||||
const { key: columnKey } = column;
|
||||
const originalOldCellValue = getCellValueByColumn(record, column);
|
||||
const updated = (this.getEditor() && this.getEditor().getValue()) || {};
|
||||
if (!checkCellValueChanged(originalOldCellValue, updated[columnKey])) {
|
||||
this.props.onCommitCancel();
|
||||
return;
|
||||
}
|
||||
this.commitData(updated, true);
|
||||
};
|
||||
|
||||
commitData = (updated, closeEditor = false) => {
|
||||
if (!this.isNewValueValid(updated)) return;
|
||||
const { onCommit, record, column } = this.props;
|
||||
const { key: columnKey, name: columnName } = column;
|
||||
this.changeCommitted = true;
|
||||
const rowId = record._id;
|
||||
const originalOldCellValue = getCellValueByColumn(record, column);
|
||||
const key = Object.keys(updated)[0];
|
||||
const value = updated[key];
|
||||
const updates = checkIsPrivateColumn(column) ? { [columnKey]: value } : { [columnName]: value };
|
||||
const { oldRowData, originalOldRowData } = this.getOldRowData(originalOldCellValue);
|
||||
|
||||
// updates used for update remote record data
|
||||
// originalUpdates used for update local record data
|
||||
// oldRowData ues for undo/undo modify record
|
||||
// originalOldRowData ues for undo/undo modify record
|
||||
onCommit({ rowId, cellKey: columnKey, updates, originalUpdates: updated, oldRowData, originalOldRowData }, closeEditor);
|
||||
};
|
||||
|
||||
commitCancel = () => {
|
||||
this.changeCanceled = true;
|
||||
this.props.onCommitCancel();
|
||||
};
|
||||
|
||||
isNewValueValid = (value) => {
|
||||
if (this.getEditor() && Utils.isFunction(this.getEditor().validate)) {
|
||||
const isValid = this.getEditor().validate(value);
|
||||
this.setState({ isInvalid: !isValid });
|
||||
return isValid;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
setCaretAtEndOfInput = () => {
|
||||
const input = this.getInputNode();
|
||||
// taken from http://stackoverflow.com/questions/511088/use-javascript-to-place-cursor-at-end-of-text-in-text-input-element
|
||||
const txtLength = input.value.length;
|
||||
if (input.setSelectionRange) {
|
||||
input.setSelectionRange(txtLength, txtLength);
|
||||
} else if (input.createTextRange) {
|
||||
const fieldRange = input.createTextRange();
|
||||
fieldRange.moveStart('character', txtLength);
|
||||
fieldRange.collapse();
|
||||
fieldRange.select();
|
||||
}
|
||||
};
|
||||
|
||||
isCaretAtBeginningOfInput = () => {
|
||||
const inputNode = this.getInputNode();
|
||||
return inputNode.selectionStart === inputNode.selectionEnd
|
||||
&& inputNode.selectionStart === 0;
|
||||
};
|
||||
|
||||
isCaretAtEndOfInput = () => {
|
||||
const inputNode = this.getInputNode();
|
||||
return inputNode.selectionStart === inputNode.value.length;
|
||||
};
|
||||
|
||||
handleRightClick = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
setTextInputFocus = () => {
|
||||
const keyCode = this.props.firstEditorKeyDown;
|
||||
const inputNode = this.getInputNode();
|
||||
inputNode.focus();
|
||||
if (inputNode.tagName === 'INPUT') {
|
||||
if (!isKeyPrintable(keyCode)) {
|
||||
inputNode.focus();
|
||||
inputNode.select();
|
||||
} else {
|
||||
inputNode.select();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClickOutside = (e) => {
|
||||
// Prevent impact on the drag handle module
|
||||
const className = getEventClassName(e);
|
||||
if (className && className.includes('drag-handle')) return;
|
||||
this.commit();
|
||||
this.props.onCommitCancel();
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { width, height, left, top } = this.props;
|
||||
const style = { position: 'absolute', height, width, left, top, zIndex: Z_INDEX_EDITOR_CONTAINER };
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.onClickOutside}>
|
||||
<div
|
||||
style={style}
|
||||
className={this.getContainerClass()}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onScroll={this.onScroll}
|
||||
onContextMenu={this.handleRightClick}
|
||||
>
|
||||
{this.createEditor()}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NormalEditorContainer.propTypes = {
|
||||
firstEditorKeyDown: PropTypes.string,
|
||||
openEditorMode: PropTypes.string,
|
||||
columns: PropTypes.array,
|
||||
|
||||
// position info
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
top: PropTypes.number,
|
||||
scrollLeft: PropTypes.number,
|
||||
scrollTop: PropTypes.number,
|
||||
|
||||
record: PropTypes.object,
|
||||
column: PropTypes.object,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object, PropTypes.bool]),
|
||||
|
||||
onGridKeyDown: PropTypes.func,
|
||||
onCommit: PropTypes.func,
|
||||
onCommitCancel: PropTypes.func,
|
||||
};
|
||||
|
||||
export default NormalEditorContainer;
|
@@ -0,0 +1,226 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { ClickOutside } from '@seafile/sf-metadata-ui-component';
|
||||
import Editor from './editor';
|
||||
import { Utils } from '../../../../utils/utils';
|
||||
import { EDITOR_CONTAINER as Z_INDEX_EDITOR_CONTAINER } from '../../constants/z-index';
|
||||
import EventBus from '../../../common/event-bus';
|
||||
import { checkIsPrivateColumn, getColumnOriginName } from '../../utils/column';
|
||||
import { checkCellValueChanged } from '../../utils/cell-comparer';
|
||||
import { getCellValueByColumn } from '../../utils/cell';
|
||||
import { EVENT_BUS_TYPE } from '../../constants/event-bus-type';
|
||||
|
||||
class PopupEditorContainer extends React.Component {
|
||||
|
||||
static displayName = 'PopupEditorContainer';
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { width, height, left, top } = this.props;
|
||||
this.state = {
|
||||
isInvalid: false,
|
||||
style: {
|
||||
position: 'absolute',
|
||||
zIndex: Z_INDEX_EDITOR_CONTAINER,
|
||||
left,
|
||||
top,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
};
|
||||
this.eventBus = EventBus.getInstance();
|
||||
this.isClosed = false;
|
||||
this.changeCanceled = false;
|
||||
}
|
||||
|
||||
changeCommitted = false;
|
||||
changeCanceled = false;
|
||||
editingRowId = this.props.record._id;
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.scrollLeft !== this.props.scrollLeft || prevProps.scrollTop !== this.props.scrollTop) {
|
||||
this.commitCancel();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (!this.changeCommitted && !this.changeCanceled) {
|
||||
this.commit();
|
||||
}
|
||||
}
|
||||
|
||||
setEditorRef = (editor) => {
|
||||
this.editor = editor;
|
||||
};
|
||||
|
||||
createEditor = () => {
|
||||
const { column, record, height, onPressTab, editorPosition, columns, modifyColumnData, readOnly } = this.props;
|
||||
const value = this.getInitialValue();
|
||||
|
||||
let editorProps = {
|
||||
ref: this.setEditorRef,
|
||||
value: value,
|
||||
recordMetaData: this.getRecordMetaData(),
|
||||
onBlur: this.commit,
|
||||
onCommit: this.commit,
|
||||
onCommitData: this.commitData,
|
||||
onCommitCancel: this.commitCancel,
|
||||
onClose: this.closeEditor,
|
||||
onEscape: this.closeEditor,
|
||||
editorContainer: document.body,
|
||||
modifyColumnData,
|
||||
editorPosition,
|
||||
record,
|
||||
height,
|
||||
columns,
|
||||
column,
|
||||
readOnly,
|
||||
onPressTab,
|
||||
};
|
||||
|
||||
return (
|
||||
<Editor column={column} editorProps={editorProps} />
|
||||
);
|
||||
};
|
||||
|
||||
getRecordMetaData = () => {
|
||||
// clone row data so editor cannot actually change this
|
||||
// convention based method to get corresponding Id or Name of any Name or Id property
|
||||
if (typeof this.props.column.getRecordMetaData === 'function') {
|
||||
const { record, column } = this.props;
|
||||
return this.props.column.getRecordMetaData(record, column);
|
||||
}
|
||||
};
|
||||
|
||||
getEditor = () => {
|
||||
return this.editor;
|
||||
};
|
||||
|
||||
getInitialValue = () => {
|
||||
const { firstEditorKeyDown: key, value } = this.props;
|
||||
if (key === 'Enter') {
|
||||
return value;
|
||||
}
|
||||
return key || value;
|
||||
};
|
||||
|
||||
getOldRowData = (originalOldCellValue) => {
|
||||
const { column } = this.props;
|
||||
const columnName = getColumnOriginName(column);
|
||||
const { key: columnKey } = column;
|
||||
let oldValue = originalOldCellValue;
|
||||
if (this.getEditor() && this.getEditor().getOldValue) {
|
||||
const original = this.getEditor().getOldValue();
|
||||
oldValue = original[Object.keys(original)[0]];
|
||||
}
|
||||
const oldRowData = { [columnName]: oldValue };
|
||||
const originalOldRowData = { [columnKey]: originalOldCellValue }; // { [column.key]: cellValue }
|
||||
return { oldRowData, originalOldRowData };
|
||||
};
|
||||
|
||||
// The input area in the interface loses focus. Use this.getEditor().getValue() to get data.
|
||||
commit = (closeEditor) => {
|
||||
const { record } = this.props;
|
||||
if (!record._id) return;
|
||||
const updated = (this.getEditor() && this.getEditor().getValue()) || {};
|
||||
this.commitData(updated, closeEditor);
|
||||
};
|
||||
|
||||
// This is the updated data obtained by manually clicking the button
|
||||
commitData = (updated, closeEditor = false) => {
|
||||
const { onCommit, column, record } = this.props;
|
||||
const { key: columnKey, name: columnName } = column;
|
||||
const originalOldCellValue = getCellValueByColumn(record, column);
|
||||
let originalUpdates = { ...updated };
|
||||
if (!checkCellValueChanged(originalOldCellValue, originalUpdates[columnKey]) || !this.isNewValueValid(updated)) {
|
||||
if (closeEditor && typeof this.editor.onClose === 'function') {
|
||||
this.editor.onClose();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.changeCommitted = true;
|
||||
const rowId = record._id;
|
||||
const key = Object.keys(updated)[0];
|
||||
const value = updated[key];
|
||||
const updates = checkIsPrivateColumn(column) ? { [columnKey]: value } : { [columnName]: value };
|
||||
const { oldRowData, originalOldRowData } = this.getOldRowData(originalOldCellValue);
|
||||
|
||||
// updates used for update remote record data
|
||||
// originalUpdates used for update local record data
|
||||
// oldRowData ues for undo/undo modify record
|
||||
// originalOldRowData ues for undo/undo modify record
|
||||
onCommit({ rowId, cellKey: columnKey, updates, originalUpdates, oldRowData, originalOldRowData }, closeEditor);
|
||||
};
|
||||
|
||||
commitCancel = () => {
|
||||
this.changeCanceled = true;
|
||||
this.props.onCommitCancel();
|
||||
};
|
||||
|
||||
isNewValueValid = (value) => {
|
||||
if (this.getEditor() && Utils.isFunction(this.getEditor().validate)) {
|
||||
const isValid = this.getEditor().validate(value);
|
||||
this.setState({ isInvalid: !isValid });
|
||||
return isValid;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
handleRightClick = (e) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
closeEditor = (isEscapeKeydown) => {
|
||||
!this.isClosed && this.onClickOutside(isEscapeKeydown);
|
||||
};
|
||||
|
||||
onClickOutside = (isEscapeKeydown) => {
|
||||
this.isClosed = true;
|
||||
this.commit();
|
||||
this.props.onCommitCancel();
|
||||
!isEscapeKeydown && this.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.onClickOutside}>
|
||||
<div
|
||||
style={this.state.style}
|
||||
className={classnames({ 'has-error': this.state.isInvalid === true })}
|
||||
onContextMenu={this.handleRightClick}
|
||||
ref={this.props.innerRef}
|
||||
>
|
||||
{this.createEditor()}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PopupEditorContainer.propTypes = {
|
||||
firstEditorKeyDown: PropTypes.string,
|
||||
openEditorMode: PropTypes.string,
|
||||
columns: PropTypes.array,
|
||||
|
||||
// position info
|
||||
editorPosition: PropTypes.object,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
top: PropTypes.number,
|
||||
scrollLeft: PropTypes.number,
|
||||
scrollTop: PropTypes.number,
|
||||
|
||||
record: PropTypes.object,
|
||||
column: PropTypes.object,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object, PropTypes.bool, PropTypes.array]),
|
||||
|
||||
onGridKeyDown: PropTypes.func,
|
||||
onCommit: PropTypes.func,
|
||||
onCommitCancel: PropTypes.func,
|
||||
innerRef: PropTypes.func,
|
||||
onPressTab: PropTypes.func,
|
||||
};
|
||||
|
||||
export default PopupEditorContainer;
|
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import Editor from './editor';
|
||||
|
||||
const PreviewEditorContainer = (props) => {
|
||||
return (
|
||||
<Editor column={props.column} editorProps={props} mode={props.openEditorMode} />
|
||||
);
|
||||
};
|
||||
|
||||
export default PreviewEditorContainer;
|
42
frontend/src/components/sf-table/editors/editor-portal.js
Normal file
42
frontend/src/components/sf-table/editors/editor-portal.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class EditorPortal extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
target: PropTypes.instanceOf(Element).isRequired
|
||||
};
|
||||
|
||||
// Keep track of when the modal element is added to the DOM
|
||||
state = {
|
||||
isMounted: false
|
||||
};
|
||||
|
||||
el = document.createElement('div');
|
||||
|
||||
componentDidMount() {
|
||||
this.props.target.appendChild(this.el);
|
||||
// eslint-disable-next-line react/no-did-mount-set-state
|
||||
this.setState({ isMounted: true });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.target.removeChild(this.el);
|
||||
}
|
||||
|
||||
render() {
|
||||
// Don't render the portal until the component has mounted,
|
||||
// So the portal can safely access the DOM.
|
||||
if (!this.state.isMounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
this.props.children,
|
||||
this.el,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EditorPortal;
|
@@ -0,0 +1,56 @@
|
||||
.sf-metadata-delete-select-tags {
|
||||
background-color: #f6f6f6;
|
||||
border-bottom: 1px solid #dde2ea;
|
||||
border-radius: 3px 3px 0 0;
|
||||
min-height: 35px;
|
||||
padding: 2px 10px;
|
||||
line-height: 1;
|
||||
max-height: 150px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.sf-metadata-delete-select-tags .sf-metadata-delete-select-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
padding: 0 8px 0 2px;
|
||||
font-size: 13px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #dbdbdb;
|
||||
background: #fff;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.sf-metadata-delete-select-tags .sf-metadata-delete-select-tag .sf-metadata-delete-select-tag-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.sf-metadata-delete-select-tags .sf-metadata-delete-select-tag .sf-metadata-delete-select-tag-name {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
color: #212529;
|
||||
margin-left: 8px;
|
||||
max-width: 200px;
|
||||
flex: 1 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sf-metadata-delete-select-tags .sf-metadata-delete-select-tag .sf-metadata-delete-select-remove {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
position: relative;
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
.sf-metadata-delete-select-tags .sf-metadata-delete-select-remove .sf-metadata-icon-x-01 {
|
||||
fill: #666;
|
||||
font-size: 12px;
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
import React from './index';
|
||||
import PropTypes from 'prop-types';
|
||||
import { IconBtn } from '@seafile/sf-metadata-ui-component';
|
||||
import { getTagColor, getTagName } from '../../../../../tag/utils/cell/core';
|
||||
import { getRowById } from '../../../utils/table';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const DeleteTag = ({ value, tagsTable, onDelete }) => {
|
||||
return (
|
||||
<div className="sf-metadata-delete-select-tags">
|
||||
{Array.isArray(value) && value.map(tagId => {
|
||||
const tag = getRowById(tagsTable, tagId);
|
||||
if (!tag) return null;
|
||||
const tagName = getTagName(tag);
|
||||
const tagColor = getTagColor(tag);
|
||||
return (
|
||||
<div className="sf-metadata-delete-select-tag" key={tagId}>
|
||||
<div className="sf-metadata-delete-select-tag-color" style={{ backgroundColor: tagColor }}></div>
|
||||
<div className="sf-metadata-delete-select-tag-name">{tagName}</div>
|
||||
<IconBtn className="sf-metadata-delete-select-remove" onClick={(event) => onDelete(tagId, event)} iconName="x-01" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DeleteTag.propTypes = {
|
||||
value: PropTypes.array,
|
||||
tagsTable: PropTypes.object,
|
||||
onDelete: PropTypes.func,
|
||||
};
|
||||
|
||||
export default DeleteTag;
|
@@ -0,0 +1,76 @@
|
||||
.sf-metadata-tags-editor {
|
||||
background-color: #fff;
|
||||
border: 1px solid #dedede;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px 0 #dedede;
|
||||
left: 0;
|
||||
min-height: 160px;
|
||||
min-width: 200px;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-search-tags-container {
|
||||
padding: 10px 10px 0;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-search-tags-container .sf-metadata-search-tags {
|
||||
font-size: 14px;
|
||||
max-height: 30px;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-container {
|
||||
max-height: 200px;
|
||||
min-height: 100px;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-container .none-search-result {
|
||||
font-size: 14px;
|
||||
opacity: 0.5;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container {
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
color: #212529;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-container-highlight {
|
||||
background: #f5f5f5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sf-metadata-tags-editor .sf-metadata-tags-editor-tag-check-icon {
|
||||
text-align: center;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.sf-metadata-tag-color-and-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sf-metadata-tag-color-and-name .sf-metadata-tag-color {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sf-metadata-tag-color-and-name .sf-metadata-tag-name {
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
275
frontend/src/components/sf-table/editors/tags-editor/index.js
Normal file
275
frontend/src/components/sf-table/editors/tags-editor/index.js
Normal file
@@ -0,0 +1,275 @@
|
||||
import React, { useMemo, useCallback, useState, useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { SearchInput, CustomizeAddTool, Icon } from '@seafile/sf-metadata-ui-component';
|
||||
import DeleteTags from './delete-tags';
|
||||
import { Utils } from '../../../../utils/utils';
|
||||
import { KeyCodes } from '../../../../constants';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
import { getTagColor, getTagId, getTagName, getTagsByNameOrColor } from '../../../../tag/utils/cell/core';
|
||||
import { getRecordIdFromRecord } from '../../../../metadata/utils/cell';
|
||||
import { SELECT_OPTION_COLORS } from '../../../../metadata/constants';
|
||||
import { getRowById } from '../../utils/table';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const TagsEditor = ({
|
||||
tagsTable,
|
||||
height,
|
||||
column,
|
||||
record,
|
||||
value: oldValue,
|
||||
editorPosition = { left: 0, top: 0 },
|
||||
canAddTag,
|
||||
onPressTab,
|
||||
addNewTag,
|
||||
selectTag,
|
||||
deselectTag,
|
||||
}) => {
|
||||
const [value, setValue] = useState((oldValue || []).map(item => item.row_id).filter(item => getRowById(tagsTable, item)));
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [highlightIndex, setHighlightIndex] = useState(-1);
|
||||
const [maxItemNum, setMaxItemNum] = useState(0);
|
||||
const itemHeight = 30;
|
||||
const allTagsRef = useRef((tagsTable && tagsTable.rows) || []);
|
||||
const editorContainerRef = useRef(null);
|
||||
const editorRef = useRef(null);
|
||||
const selectItemRef = useRef(null);
|
||||
|
||||
const displayTags = useMemo(() => getTagsByNameOrColor(allTagsRef.current, searchValue), [searchValue, allTagsRef]);
|
||||
|
||||
const isShowCreateBtn = useMemo(() => {
|
||||
if (!canAddTag || !searchValue || !Utils.isFunction(addNewTag)) return false;
|
||||
return displayTags.length === 0;
|
||||
}, [displayTags, searchValue, canAddTag, addNewTag]);
|
||||
|
||||
const style = useMemo(() => {
|
||||
return { width: column.width };
|
||||
}, [column]);
|
||||
|
||||
const onChangeSearch = useCallback((newSearchValue) => {
|
||||
if (searchValue === newSearchValue) return;
|
||||
setSearchValue(newSearchValue);
|
||||
}, [searchValue]);
|
||||
|
||||
const onSelectTag = useCallback((tagId) => {
|
||||
const recordId = getRecordIdFromRecord(record);
|
||||
const newValue = value.slice(0);
|
||||
let optionIdx = value.indexOf(tagId);
|
||||
if (optionIdx > -1) {
|
||||
newValue.splice(optionIdx, 1);
|
||||
deselectTag && deselectTag(tagId, recordId);
|
||||
} else {
|
||||
newValue.push(tagId);
|
||||
selectTag && selectTag(tagId, recordId);
|
||||
}
|
||||
setValue(newValue);
|
||||
}, [value, record, selectTag, deselectTag]);
|
||||
|
||||
const onDeleteTag = useCallback((tagId) => {
|
||||
const recordId = getRecordIdFromRecord(record);
|
||||
const newValue = value.slice(0);
|
||||
let optionIdx = value.indexOf(tagId);
|
||||
if (optionIdx > -1) {
|
||||
newValue.splice(optionIdx, 1);
|
||||
}
|
||||
deselectTag && deselectTag(tagId, recordId);
|
||||
setValue(newValue);
|
||||
}, [value, record, deselectTag]);
|
||||
|
||||
const onMenuMouseEnter = useCallback((highlightIndex) => {
|
||||
setHighlightIndex(highlightIndex);
|
||||
}, []);
|
||||
|
||||
const onMenuMouseLeave = useCallback((index) => {
|
||||
setHighlightIndex(-1);
|
||||
}, []);
|
||||
|
||||
const createTag = useCallback((event) => {
|
||||
event && event.stopPropagation();
|
||||
event && event.nativeEvent.stopImmediatePropagation();
|
||||
const defaultOptions = SELECT_OPTION_COLORS.slice(0, 24);
|
||||
const defaultOption = defaultOptions[Math.floor(Math.random() * defaultOptions.length)];
|
||||
addNewTag({ tagName: searchValue, tagColor: defaultOption.COLOR }, {
|
||||
success_callback: (newTag) => {
|
||||
const recordId = getRecordIdFromRecord(record);
|
||||
const newValue = [...value, newTag];
|
||||
selectTag && selectTag(newTag._id, recordId);
|
||||
setValue(newValue);
|
||||
},
|
||||
fail_callback: () => {},
|
||||
});
|
||||
}, [value, searchValue, record, addNewTag, selectTag]);
|
||||
|
||||
const getMaxItemNum = useCallback(() => {
|
||||
let selectContainerStyle = getComputedStyle(editorContainerRef.current, null);
|
||||
let selectItemStyle = getComputedStyle(selectItemRef.current, null);
|
||||
let maxSelectItemNum = Math.floor(parseInt(selectContainerStyle.maxHeight) / parseInt(selectItemStyle.height));
|
||||
return maxSelectItemNum - 1;
|
||||
}, [editorContainerRef, selectItemRef]);
|
||||
|
||||
const onEnter = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
let tag;
|
||||
if (displayTags.length === 1) {
|
||||
tag = displayTags[0];
|
||||
} else if (highlightIndex > -1) {
|
||||
tag = displayTags[highlightIndex];
|
||||
}
|
||||
if (tag) {
|
||||
const newTagId = getTagId(tag);
|
||||
onSelectTag(newTagId);
|
||||
return;
|
||||
}
|
||||
if (isShowCreateBtn) {
|
||||
createTag();
|
||||
}
|
||||
}, [displayTags, highlightIndex, isShowCreateBtn, onSelectTag, createTag]);
|
||||
|
||||
const onUpArrow = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (highlightIndex === 0) return;
|
||||
setHighlightIndex(highlightIndex - 1);
|
||||
if (highlightIndex > displayTags.length - maxItemNum) {
|
||||
editorContainerRef.current.scrollTop -= itemHeight;
|
||||
}
|
||||
}, [editorContainerRef, highlightIndex, maxItemNum, displayTags, itemHeight]);
|
||||
|
||||
const onDownArrow = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (highlightIndex === displayTags.length - 1) return;
|
||||
setHighlightIndex(highlightIndex + 1);
|
||||
if (highlightIndex >= maxItemNum) {
|
||||
editorContainerRef.current.scrollTop += itemHeight;
|
||||
}
|
||||
}, [editorContainerRef, highlightIndex, maxItemNum, displayTags, itemHeight]);
|
||||
|
||||
const onHotKey = useCallback((event) => {
|
||||
if (event.keyCode === KeyCodes.Enter) {
|
||||
onEnter(event);
|
||||
} else if (event.keyCode === KeyCodes.UpArrow) {
|
||||
onUpArrow(event);
|
||||
} else if (event.keyCode === KeyCodes.DownArrow) {
|
||||
onDownArrow(event);
|
||||
} else if (event.keyCode === KeyCodes.Tab) {
|
||||
if (Utils.isFunction(onPressTab)) {
|
||||
onPressTab(event);
|
||||
}
|
||||
}
|
||||
}, [onEnter, onUpArrow, onDownArrow, onPressTab]);
|
||||
|
||||
const onKeyDown = useCallback((event) => {
|
||||
if (
|
||||
event.keyCode === KeyCodes.ChineseInputMethod ||
|
||||
event.keyCode === KeyCodes.Enter ||
|
||||
event.keyCode === KeyCodes.LeftArrow ||
|
||||
event.keyCode === KeyCodes.RightArrow
|
||||
) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
const { bottom } = editorRef.current.getBoundingClientRect();
|
||||
if (bottom > window.innerHeight) {
|
||||
editorRef.current.style.top = 'unset';
|
||||
editorRef.current.style.bottom = editorPosition.top + height - window.innerHeight + 'px';
|
||||
}
|
||||
}
|
||||
if (editorContainerRef.current && selectItemRef.current) {
|
||||
setMaxItemNum(getMaxItemNum());
|
||||
}
|
||||
document.addEventListener('keydown', onHotKey, true);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onHotKey, true);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onHotKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const highlightIndex = displayTags.length === 0 ? -1 : 0;
|
||||
setHighlightIndex(highlightIndex);
|
||||
}, [displayTags]);
|
||||
|
||||
const renderOptions = useCallback(() => {
|
||||
if (displayTags.length === 0) {
|
||||
const noOptionsTip = searchValue ? gettext('No tags available') : gettext('No tag');
|
||||
return (<span className="none-search-result">{noOptionsTip}</span>);
|
||||
}
|
||||
|
||||
return displayTags.map((tag, i) => {
|
||||
const tagId = getTagId(tag);
|
||||
const tagName = getTagName(tag);
|
||||
const tagColor = getTagColor(tag);
|
||||
const isSelected = Array.isArray(value) ? value.includes(tagId) : false;
|
||||
return (
|
||||
<div key={tagId} className="sf-metadata-tags-editor-tag-item" ref={selectItemRef}>
|
||||
<div
|
||||
className={classnames('sf-metadata-tags-editor-tag-container pl-2', { 'sf-metadata-tags-editor-tag-container-highlight': i === highlightIndex })}
|
||||
onMouseDown={() => onSelectTag(tagId)}
|
||||
onMouseEnter={() => onMenuMouseEnter(i)}
|
||||
onMouseLeave={() => onMenuMouseLeave(i)}
|
||||
>
|
||||
<div className="sf-metadata-tag-color-and-name">
|
||||
<div className="sf-metadata-tag-color" style={{ backgroundColor: tagColor }}></div>
|
||||
<div className="sf-metadata-tag-name">{tagName}</div>
|
||||
</div>
|
||||
<div className="sf-metadata-tags-editor-tag-check-icon">
|
||||
{isSelected && (<Icon iconName="check-mark" />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
}, [displayTags, searchValue, value, highlightIndex, onMenuMouseEnter, onMenuMouseLeave, onSelectTag]);
|
||||
|
||||
return (
|
||||
<div className="sf-metadata-tags-editor" style={style} ref={editorRef}>
|
||||
<DeleteTags value={value} tagsTable={tagsTable} onDelete={onDeleteTag} />
|
||||
<div className="sf-metadata-search-tags-container">
|
||||
<SearchInput
|
||||
placeholder={gettext('Search tag')}
|
||||
onKeyDown={onKeyDown}
|
||||
onChange={onChangeSearch}
|
||||
autoFocus={true}
|
||||
className="sf-metadata-search-tags"
|
||||
/>
|
||||
</div>
|
||||
<div className="sf-metadata-tags-editor-container" ref={editorContainerRef}>
|
||||
{renderOptions()}
|
||||
</div>
|
||||
{isShowCreateBtn && (
|
||||
<CustomizeAddTool
|
||||
callBack={createTag}
|
||||
footerName={`${gettext('Add tag')} ${searchValue}`}
|
||||
className="add-search-result"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TagsEditor.propTypes = {
|
||||
tagsTable: PropTypes.object,
|
||||
height: PropTypes.number,
|
||||
record: PropTypes.object,
|
||||
column: PropTypes.object,
|
||||
value: PropTypes.array,
|
||||
editorPosition: PropTypes.object,
|
||||
canAddTag: PropTypes.bool,
|
||||
onPressTab: PropTypes.func,
|
||||
addNewTag: PropTypes.func,
|
||||
selectTag: PropTypes.func,
|
||||
deselectTag: PropTypes.func,
|
||||
};
|
||||
|
||||
TagsEditor.defaultProps = {
|
||||
canAddTag: false,
|
||||
};
|
||||
|
||||
export default TagsEditor;
|
391
frontend/src/components/sf-table/index.css
Normal file
391
frontend/src/components/sf-table/index.css
Normal file
@@ -0,0 +1,391 @@
|
||||
.sf-table-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transform: translateZ(10px);
|
||||
}
|
||||
|
||||
.sf-table-main-container {
|
||||
display: flex;
|
||||
flex: 1 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sf-table-result-container {
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.sf-table-result-content {
|
||||
height: 100%;
|
||||
min-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sf-table-result-content .static-sf-table-result-content {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background: #f9f9f9;
|
||||
border-bottom: 1px solid #ddd;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.sf-table-result-content .static-sf-table-result-content .sf-table-row {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.record-HeaderCell__draggable {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: -3px;
|
||||
width: 5px;
|
||||
border-radius: 3px;
|
||||
margin: 3px 0px;
|
||||
height: 80%;
|
||||
cursor: col-resize;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.record-HeaderCell__draggable:hover {
|
||||
background-color: #2d7ff9;
|
||||
}
|
||||
|
||||
.sf-table-header-cell .sf-table-cell {
|
||||
overflow: unset;
|
||||
}
|
||||
|
||||
.sf-table-row {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #ddd;
|
||||
display: inline-flex;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.static-sf-table-result-content.grid-header .sf-table-row {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sf-table-result-content .sf-table-row {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.sf-table-result-content .frozen-columns .sf-table-cell {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sf-table-column-content {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.sf-table-column-content.row-index {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sf-table-column-content .sf-metadata-font {
|
||||
font-size: 14px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.sf-table-column-content .header-name {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sf-table-column-content .header-name-text {
|
||||
line-height: 1.5;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sf-table-column-content .header-name-text.double {
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-height: 46px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.sf-table-canvas {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
min-width: 100%;
|
||||
padding-bottom: 150px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.sf-table-canvas .sf-metadata-result-loading {
|
||||
position: absolute;
|
||||
left: 50vw;
|
||||
}
|
||||
|
||||
.sf-table-records-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.draging-file-to-cell .cell-file-add {
|
||||
border: 1px dashed #f09f3f;
|
||||
background-color: #fdf5eb !important;
|
||||
}
|
||||
|
||||
.table-main-interval {
|
||||
height: 20px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.sf-table-tooltip .tooltip-inner {
|
||||
max-width: 242px;
|
||||
font-weight: lighter;
|
||||
text-align: start;
|
||||
background-color: #303133;
|
||||
}
|
||||
|
||||
.sf-table-tooltip .bs-tooltip-right .arrow::before,
|
||||
.sf-table-tooltip .bs-tooltip-auto[x-placement^="right"] .arrow::before {
|
||||
border-right-color: #303133;
|
||||
}
|
||||
|
||||
.sf-table-tooltip .bs-tooltip-top .arrow::before,
|
||||
.sf-table-tooltip .bs-tooltip-auto[x-placement^="top"] .arrow::before {
|
||||
border-top-color: #303133;
|
||||
}
|
||||
|
||||
.sf-table-tooltip .bs-tooltip-bottom .arrow::before,
|
||||
.sf-table-tooltip .bs-tooltip-auto[x-placement^="bottom"] .arrow::before {
|
||||
border-bottom-color: #303133;
|
||||
}
|
||||
|
||||
.sf-table-tooltip .bs-tooltip-left .arrow::before,
|
||||
.sf-table-tooltip .bs-tooltip-auto[x-placement^="left"] .arrow::before {
|
||||
border-left-color: #303133;
|
||||
}
|
||||
|
||||
.add-item-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-top: 1px solid #dedede;
|
||||
background: #fff;
|
||||
padding: 0 1rem;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
.add-item-btn:hover {
|
||||
cursor: pointer;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.add-item-btn .sf-metadata-icon-add-table {
|
||||
margin-right: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.formula-formatter-content-item {
|
||||
margin-right: 10px;
|
||||
font-size: 13px;
|
||||
max-width: 100%;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.formula-formatter-content-item.simple-cell-formatter {
|
||||
height: 20px;
|
||||
padding: 0 8px;
|
||||
align-items: center;
|
||||
background: #eceff4;
|
||||
border-radius: 3px;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.formula-formatter-content-item.simple-cell-text-formatter {
|
||||
height: fit-content;
|
||||
min-height: 20px;
|
||||
padding: 0 8px;
|
||||
align-items: center;
|
||||
background: #eceff4;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.formula-ellipsis .sf-metadata-font {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.formula-formatter-content-item .checkbox {
|
||||
transform: unset;
|
||||
}
|
||||
|
||||
.formula-formatter-content-item .collaborator {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.checkbox-editor-rows-container .grid-checkbox-row-checkbox {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* common */
|
||||
.sf-metadata-result-container.horizontal-scroll .table-last--frozen {
|
||||
box-shadow: 2px 0 5px -2px rgb(136 136 136 / 30%) !important;
|
||||
}
|
||||
|
||||
.table-last--frozen {
|
||||
border-right: 1px solid #cacaca !important;
|
||||
}
|
||||
|
||||
.row-locked .actions-cell::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
content: '';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 10px solid #f25041;
|
||||
border-left: 10px solid transparent;
|
||||
}
|
||||
|
||||
/* table cell editor */
|
||||
.table-cell-editor .number-editor,
|
||||
.table-cell-editor .duration-editor {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.table-cell-editor .checkbox-editor-rows-container {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-cell-editor .rate-editor .rate-item {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.rdg-editor-container .rate-editor {
|
||||
width: 100%;
|
||||
height: calc(100% - 1px);
|
||||
border: 2px solid #66afe9;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
/* background-color: #fff; */
|
||||
box-sizing: border-box;
|
||||
padding: 0 8px 2px 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rdg-editor-container .checkbox-editor-rows-container {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.geolocation-editor-container {
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 0 5px #ccc;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
min-width: max-content;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.geolocation-editor-container .geolocation-selector-container {
|
||||
position: absolute;
|
||||
min-width: 400px;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 0 5px #ccc;
|
||||
min-height: 165px;
|
||||
}
|
||||
|
||||
.geolocation-editor-container .geolocation-selector-header {
|
||||
height: 45px;
|
||||
display: flex;
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 5px 20px 0 20px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.geolocation-editor-container .geolocation-selector-header-item {
|
||||
border: 1px solid #ccc;
|
||||
height: 35px;
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
margin-bottom: -1px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
padding: 10px;
|
||||
line-height: 15px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.geolocation-editor-container .selected-geolocation-selector-header-item {
|
||||
border-bottom: 1px solid #fff;
|
||||
}
|
||||
|
||||
.geolocation-editor-container .geolocation-map-editor {
|
||||
height: 384px;
|
||||
width: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sf-metadata-wrapper .sf-metadata-main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sf-metadata-wrapper .sf-metadata-main .sf-metadata-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sf-metadata-wrapper .sf-metadata-main .sf-metadata-container-transform {
|
||||
transform: translateZ(10px);
|
||||
}
|
||||
|
||||
.sf-table-records-wrapper .group-header-left .formatter-show {
|
||||
display: inline-flex;
|
||||
flex: 1;
|
||||
width: calc(100% - 15px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sf-table-records-wrapper .group-header-left .sf-metadata-link-formatter {
|
||||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.sf-table-records-wrapper .group-header-left .sf-metadata-link-formatter .sf-metadata-link-item {
|
||||
max-width: 230px;
|
||||
}
|
||||
|
||||
.sf-table-header-cell .sf-table-dropdown {
|
||||
padding: 0 5px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.sf-metadata-tip-default {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
148
frontend/src/components/sf-table/index.js
Normal file
148
frontend/src/components/sf-table/index.js
Normal file
@@ -0,0 +1,148 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import TableMain from './table-main';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const SFTable = ({
|
||||
table,
|
||||
visibleColumns,
|
||||
headerSettings,
|
||||
recordsIds,
|
||||
groupbys,
|
||||
groups,
|
||||
showSequenceColumn,
|
||||
isGroupView,
|
||||
noRecordsTipsText,
|
||||
isLoadingMoreRecords,
|
||||
hasMoreRecords,
|
||||
showGridFooter,
|
||||
onGridKeyDown,
|
||||
onGridKeyUp,
|
||||
loadMore,
|
||||
loadAll,
|
||||
...customProps
|
||||
}) => {
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const getTableContentRect = useCallback(() => {
|
||||
return containerRef.current?.getBoundingClientRect() || { x: 0, right: window.innerWidth };
|
||||
}, [containerRef]);
|
||||
|
||||
const recordGetterById = useCallback((recordId) => {
|
||||
return table.id_row_map[recordId];
|
||||
}, [table]);
|
||||
|
||||
const recordGetter = useCallback((recordIndex) => {
|
||||
const recordId = recordsIds[recordIndex];
|
||||
return recordId && recordGetterById(recordId);
|
||||
}, [recordsIds, recordGetterById]);
|
||||
|
||||
const groupRecordGetter = useCallback((groupRecordIndex) => {
|
||||
if (!window.sfTableBody || !window.sfTableBody.getGroupRecordByIndex) return null;
|
||||
const groupRecord = window.sfTableBody.getGroupRecordByIndex(groupRecordIndex);
|
||||
const recordId = groupRecord.rowId;
|
||||
return recordId && recordGetterById(recordId);
|
||||
}, [recordGetterById]);
|
||||
|
||||
const recordGetterByIndex = useCallback(({ isGroupView, groupRecordIndex, recordIndex }) => {
|
||||
if (isGroupView) return groupRecordGetter(groupRecordIndex);
|
||||
return recordGetter(recordIndex);
|
||||
}, [groupRecordGetter, recordGetter]);
|
||||
|
||||
const beforeUnloadHandler = useCallback(() => {
|
||||
if (window.sfTableBody) {
|
||||
window.sfTableBody.storeScrollPosition && window.sfTableBody.storeScrollPosition();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('beforeunload', beforeUnloadHandler, false);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={classnames('sf-table-wrapper', { 'no-sequence-column': !showSequenceColumn })} ref={containerRef}>
|
||||
<TableMain
|
||||
{...customProps}
|
||||
table={table}
|
||||
visibleColumns={visibleColumns}
|
||||
headerSettings={headerSettings}
|
||||
recordsIds={recordsIds}
|
||||
groupbys={groupbys}
|
||||
groups={groups}
|
||||
showSequenceColumn={showSequenceColumn}
|
||||
isGroupView={isGroupView}
|
||||
noRecordsTipsText={noRecordsTipsText}
|
||||
hasMoreRecords={hasMoreRecords}
|
||||
isLoadingMoreRecords={isLoadingMoreRecords}
|
||||
showGridFooter={showGridFooter}
|
||||
loadMore={loadMore}
|
||||
loadAll={loadAll}
|
||||
getTableContentRect={getTableContentRect}
|
||||
onGridKeyDown={onGridKeyDown}
|
||||
onGridKeyUp={onGridKeyUp}
|
||||
recordGetterById={recordGetterById}
|
||||
recordGetterByIndex={recordGetterByIndex}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SFTable.propTypes = {
|
||||
/**
|
||||
* table: { _id, rows, id_row_map, columns, ... }
|
||||
*/
|
||||
table: PropTypes.object,
|
||||
recordsIds: PropTypes.array,
|
||||
/**
|
||||
* columns: [column, ...]
|
||||
* column: {
|
||||
* key, name, width, frozen, icon_name, is_name_column, is_private, editable, formatter, editor, editable,
|
||||
* editable_via_click_cell,
|
||||
* is_popup_editor, e.g. options-selector
|
||||
* is_support_direct_edit, e.g. checkbox
|
||||
* is_support_preview, e.g. image
|
||||
* ...
|
||||
* }
|
||||
*/
|
||||
visibleColumns: PropTypes.array.isRequired,
|
||||
headerSettings: PropTypes.object,
|
||||
showSequenceColumn: PropTypes.bool,
|
||||
groupbys: PropTypes.array,
|
||||
groups: PropTypes.array,
|
||||
isGroupView: PropTypes.bool,
|
||||
noRecordsTipsText: PropTypes.string,
|
||||
hasMoreRecords: PropTypes.bool,
|
||||
isLoadingMoreRecords: PropTypes.bool,
|
||||
showGridFooter: PropTypes.bool,
|
||||
canModifyRecords: PropTypes.bool,
|
||||
supportCopy: PropTypes.bool,
|
||||
supportCut: PropTypes.bool,
|
||||
supportPaste: PropTypes.bool,
|
||||
supportDragFill: PropTypes.bool,
|
||||
checkCanModifyRecord: PropTypes.func,
|
||||
checkCellValueChanged: PropTypes.func, // for complex cell value compare
|
||||
onGridKeyDown: PropTypes.func,
|
||||
onGridKeyUp: PropTypes.func,
|
||||
loadMore: PropTypes.func,
|
||||
loadAll: PropTypes.func,
|
||||
};
|
||||
|
||||
SFTable.defaultProps = {
|
||||
recordsIds: [],
|
||||
groupbys: [],
|
||||
groups: [],
|
||||
isGroupView: false,
|
||||
showSequenceColumn: true,
|
||||
hasMoreRecords: false,
|
||||
isLoadingMoreRecords: false,
|
||||
showGridFooter: true,
|
||||
canModifyRecords: false,
|
||||
supportCopy: false,
|
||||
supportCut: false,
|
||||
supportPaste: false,
|
||||
supportDragFill: false,
|
||||
};
|
||||
|
||||
export default SFTable;
|
56
frontend/src/components/sf-table/masks/cell-mask.js
Normal file
56
frontend/src/components/sf-table/masks/cell-mask.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class CellMask extends React.PureComponent {
|
||||
|
||||
componentDidUpdate() {
|
||||
// Scrolling left and right causes the interface to re-render,
|
||||
// and the style of CellMask is reset and needs to be fixed
|
||||
const dom = ReactDOM.findDOMNode(this);
|
||||
if (dom.style.position === 'fixed') {
|
||||
dom.style.transform = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
getMaskStyle = () => {
|
||||
const { width, height, top, left, zIndex } = this.props;
|
||||
// mask border needs to cover cell border, height and width are increased 1, left and top are decreased 1
|
||||
return {
|
||||
height: height - 1,
|
||||
width: width,
|
||||
zIndex,
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
transform: `translate(${left}px, ${top}px)`,
|
||||
outline: 0
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
const { width, height, top, left, zIndex, children, innerRef, ...rest } = this.props;
|
||||
const style = this.getMaskStyle();
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
data-test="cell-mask"
|
||||
ref={innerRef}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CellMask.propTypes = {
|
||||
width: PropTypes.number.isRequired,
|
||||
height: PropTypes.number.isRequired,
|
||||
top: PropTypes.number.isRequired,
|
||||
left: PropTypes.number.isRequired,
|
||||
zIndex: PropTypes.number.isRequired,
|
||||
children: PropTypes.node,
|
||||
innerRef: PropTypes.func
|
||||
};
|
||||
|
||||
export default CellMask;
|
20
frontend/src/components/sf-table/masks/drag-handler.js
Normal file
20
frontend/src/components/sf-table/masks/drag-handler.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function DragHandler({ onDragStart, onDragEnd }) {
|
||||
return (
|
||||
<div
|
||||
className="drag-handle"
|
||||
draggable="true"
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
DragHandler.propTypes = {
|
||||
onDragStart: PropTypes.func.isRequired,
|
||||
onDragEnd: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default DragHandler;
|
31
frontend/src/components/sf-table/masks/drag-mask.js
Normal file
31
frontend/src/components/sf-table/masks/drag-mask.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CellMask from './cell-mask';
|
||||
|
||||
function DragMask({ draggedRange, getSelectedRangeDimensions, getSelectedDimensions }) {
|
||||
const { overRecordIdx, bottomRight } = draggedRange;
|
||||
const { idx: endColumnIdx, rowIdx: endRowIdx, groupRecordIndex: endGroupRowIndex } = bottomRight;
|
||||
if (overRecordIdx !== null && endRowIdx < overRecordIdx) {
|
||||
let dimensions = getSelectedRangeDimensions(draggedRange);
|
||||
for (let currentRowIdx = endRowIdx + 1; currentRowIdx <= overRecordIdx; currentRowIdx++) {
|
||||
const { height } = getSelectedDimensions({ idx: endColumnIdx, rowIdx: currentRowIdx, groupRecordIndex: endGroupRowIndex });
|
||||
dimensions.height += height;
|
||||
}
|
||||
return (
|
||||
<CellMask
|
||||
{...dimensions}
|
||||
className='react-grid-cell-dragged-over-down'
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
DragMask.propTypes = {
|
||||
draggedRange: PropTypes.object.isRequired,
|
||||
getSelectedRangeDimensions: PropTypes.func.isRequired,
|
||||
getSelectedDimensions: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DragMask;
|
@@ -0,0 +1,54 @@
|
||||
.interaction-mask .rdg-selected {
|
||||
border: 2px solid #66afe9;
|
||||
}
|
||||
|
||||
.interaction-mask .rdg-selected-range {
|
||||
border: 1px solid #66afe9;
|
||||
background-color: rgba(102, 175, 233, 0.18823529411764706);
|
||||
}
|
||||
|
||||
.rdg-selected .drag-handle,
|
||||
.rdg-selected-range .drag-handle,
|
||||
.checkbox-editor-container .drag-handle {
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
right: -4px;
|
||||
background: #66afe9;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border: 1px solid #fff;
|
||||
border-right: 0px;
|
||||
border-bottom: 0px;
|
||||
cursor: crosshair;
|
||||
cursor: -moz-grab;
|
||||
cursor: -webkit-grab;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.rdg-selected .drag-handle:hover,
|
||||
.rdg-selected-range .drag-handle:hover,
|
||||
.checkbox-editor-container .drag-handle:hover {
|
||||
bottom: -8px;
|
||||
right: -7px;
|
||||
background: white;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid #66afe9;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.rdg-selected:hover .drag-handle .glyphicon-arrow-down {
|
||||
display: 'block';
|
||||
}
|
||||
|
||||
.react-grid-cell-dragged-over-down {
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
.react-grid-cell-dragged-over-up,
|
||||
.react-grid-cell-dragged-over-down {
|
||||
border: 1px dashed black;
|
||||
background: rgba(0, 0, 255, 0.2) !important;
|
||||
}
|
||||
|
1141
frontend/src/components/sf-table/masks/interaction-masks/index.js
Normal file
1141
frontend/src/components/sf-table/masks/interaction-masks/index.js
Normal file
File diff suppressed because it is too large
Load Diff
26
frontend/src/components/sf-table/masks/selection-mask.js
Normal file
26
frontend/src/components/sf-table/masks/selection-mask.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CellMask from './cell-mask';
|
||||
|
||||
function SelectionMask({ innerRef, selectedPosition, getSelectedDimensions, children }) {
|
||||
const dimensions = getSelectedDimensions(selectedPosition);
|
||||
return (
|
||||
<CellMask
|
||||
className="rdg-selected"
|
||||
tabIndex="0"
|
||||
innerRef={innerRef}
|
||||
{...dimensions}
|
||||
>
|
||||
{children}
|
||||
</CellMask>
|
||||
);
|
||||
}
|
||||
|
||||
SelectionMask.propTypes = {
|
||||
selectedPosition: PropTypes.object.isRequired,
|
||||
getSelectedDimensions: PropTypes.func.isRequired,
|
||||
innerRef: PropTypes.func.isRequired,
|
||||
children: PropTypes.element,
|
||||
};
|
||||
|
||||
export default SelectionMask;
|
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CellMask from './cell-mask';
|
||||
|
||||
function SelectionRangeMask({ selectedRange, innerRef, getSelectedRangeDimensions, children }) {
|
||||
const dimensions = getSelectedRangeDimensions(selectedRange);
|
||||
return (
|
||||
<CellMask
|
||||
{...dimensions}
|
||||
className="rdg-selected-range"
|
||||
innerRef={innerRef}
|
||||
>
|
||||
{children}
|
||||
</CellMask>
|
||||
);
|
||||
}
|
||||
|
||||
SelectionRangeMask.propTypes = {
|
||||
selectedRange: PropTypes.shape({
|
||||
topLeft: PropTypes.shape({ idx: PropTypes.number.isRequired, rowIdx: PropTypes.number.isRequired }).isRequired,
|
||||
bottomRight: PropTypes.shape({ idx: PropTypes.number.isRequired, rowIdx: PropTypes.number.isRequired }).isRequired,
|
||||
startCell: PropTypes.shape({ idx: PropTypes.number.isRequired, rowIdx: PropTypes.number.isRequired }).isRequired,
|
||||
cursorCell: PropTypes.shape({ idx: PropTypes.number.isRequired, rowIdx: PropTypes.number.isRequired })
|
||||
}).isRequired,
|
||||
columns: PropTypes.array.isRequired,
|
||||
rowHeight: PropTypes.number.isRequired,
|
||||
children: PropTypes.element,
|
||||
innerRef: PropTypes.func.isRequired,
|
||||
getSelectedRangeDimensions: PropTypes.func
|
||||
};
|
||||
|
||||
export default SelectionRangeMask;
|
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SCROLL_BAR as Z_INDEX_SCROLL_BAR } from '../constants/z-index';
|
||||
|
||||
const propTypes = {
|
||||
innerWidth: PropTypes.number,
|
||||
onScrollbarScroll: PropTypes.func.isRequired,
|
||||
onScrollbarMouseUp: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class HorizontalScrollbar extends React.Component {
|
||||
|
||||
isSelfScroll = true;
|
||||
|
||||
setScrollLeft = (scrollLeft) => {
|
||||
this.isSelfScroll = false;
|
||||
this.container.scrollLeft = scrollLeft;
|
||||
};
|
||||
|
||||
onScroll = (event) => {
|
||||
// only update grid's scrollLeft via scroll by itself.
|
||||
// e.g. forbid to update grid's scrollLeft when the scrollbar's scrollLeft changed by other component
|
||||
event.stopPropagation();
|
||||
if (!this.isSelfScroll) {
|
||||
this.isSelfScroll = true;
|
||||
return;
|
||||
}
|
||||
const { scrollLeft } = event.target;
|
||||
this.props.onScrollbarScroll(scrollLeft);
|
||||
return;
|
||||
};
|
||||
|
||||
getScrollbarStyle = () => {
|
||||
return { width: this.props.innerWidth };
|
||||
};
|
||||
|
||||
getContainerStyle = () => {
|
||||
return { zIndex: Z_INDEX_SCROLL_BAR };
|
||||
};
|
||||
|
||||
setScrollbarRef = (ref) => {
|
||||
this.scrollbar = ref;
|
||||
};
|
||||
|
||||
setContainerRef = (ref) => {
|
||||
this.container = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.innerWidth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const containerStyle = this.getContainerStyle();
|
||||
const scrollbarStyle = this.getScrollbarStyle();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="horizontal-scrollbar-container"
|
||||
ref={this.setContainerRef}
|
||||
style={containerStyle}
|
||||
onScroll={this.onScroll}
|
||||
onMouseUp={this.props.onScrollbarMouseUp}
|
||||
>
|
||||
<div className="horizontal-scrollbar-inner" ref={this.setScrollbarRef} style={scrollbarStyle}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalScrollbar.propTypes = propTypes;
|
||||
|
||||
export default HorizontalScrollbar;
|
9
frontend/src/components/sf-table/scrollbar/index.js
Normal file
9
frontend/src/components/sf-table/scrollbar/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import RightScrollbar from './right-scrollbar';
|
||||
import HorizontalScrollbar from './horizontal-scrollbar';
|
||||
|
||||
import './scrollbar.css';
|
||||
|
||||
export {
|
||||
RightScrollbar,
|
||||
HorizontalScrollbar,
|
||||
};
|
@@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SCROLL_BAR as Z_INDEX_SCROLL_BAR } from '../constants/z-index';
|
||||
|
||||
const propTypes = {
|
||||
getScrollHeight: PropTypes.func.isRequired,
|
||||
onScrollbarScroll: PropTypes.func.isRequired,
|
||||
onScrollbarMouseUp: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
class RightScrollbar extends React.Component {
|
||||
|
||||
isSelfScroll = true;
|
||||
|
||||
setScrollTop = (scrollTop) => {
|
||||
this.isSelfScroll = false;
|
||||
this.rightScrollContainer.scrollTop = scrollTop;
|
||||
};
|
||||
|
||||
onScroll = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
// only update canvas's scrollTop via scroll by itself.
|
||||
// e.g. forbid to update canvas's scrollTop when the scrollbar's scrollTop changed by other component
|
||||
if (!this.isSelfScroll) {
|
||||
this.isSelfScroll = true;
|
||||
return;
|
||||
}
|
||||
const { scrollTop } = event.target;
|
||||
this.props.onScrollbarScroll(scrollTop);
|
||||
};
|
||||
|
||||
onMouseUp = (event) => {
|
||||
if (this.props.onScrollbarMouseUp) {
|
||||
this.props.onScrollbarMouseUp(event);
|
||||
}
|
||||
};
|
||||
|
||||
getScrollbarStyle = () => {
|
||||
if (this.props.getScrollHeight) {
|
||||
return { height: this.props.getScrollHeight() };
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
getContainerStyle = () => {
|
||||
const style = {};
|
||||
if (this.props.getClientHeight) {
|
||||
style.height = this.props.getClientHeight();
|
||||
style.zIndex = Z_INDEX_SCROLL_BAR;
|
||||
}
|
||||
|
||||
/* sf-table-header have 33px height */
|
||||
style.top = 33;
|
||||
|
||||
/* sf-table-wrapper have 0px margin */
|
||||
style.right = 0;
|
||||
return style;
|
||||
};
|
||||
|
||||
setScrollbarRef = (ref) => {
|
||||
this.scrollbar = ref;
|
||||
};
|
||||
|
||||
setContainerRef = (ref) => {
|
||||
this.rightScrollContainer = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
const containerStyle = this.getContainerStyle();
|
||||
const scrollbarStyle = this.getScrollbarStyle();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="right-scrollbar-container"
|
||||
style={containerStyle}
|
||||
ref={this.setContainerRef}
|
||||
onScroll={this.onScroll}
|
||||
onMouseUp={this.onMouseUp}
|
||||
>
|
||||
<div ref={this.setScrollbarRef} className="right-scrollbar-inner" style={scrollbarStyle}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RightScrollbar.propTypes = propTypes;
|
||||
|
||||
export default RightScrollbar;
|
32
frontend/src/components/sf-table/scrollbar/scrollbar.css
Normal file
32
frontend/src/components/sf-table/scrollbar/scrollbar.css
Normal file
@@ -0,0 +1,32 @@
|
||||
.sf-metadata-result-container.windows-browser::-webkit-scrollbar,
|
||||
.sf-table-canvas::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.horizontal-scrollbar-container {
|
||||
height: 20px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 30px;
|
||||
background-color: transparent;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.horizontal-scrollbar-inner {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.right-scrollbar-container {
|
||||
width: 20px;
|
||||
position: fixed;
|
||||
overflow: auto;
|
||||
transition: right 0.25s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.right-scrollbar-container .right-scrollbar-inner {
|
||||
width: 1px;
|
||||
}
|
124
frontend/src/components/sf-table/table-main/index.js
Normal file
124
frontend/src/components/sf-table/table-main/index.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import EmptyTip from '../../empty-tip';
|
||||
import Records from './records';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
import { GROUP_VIEW_OFFSET } from '../constants/group';
|
||||
import { SEQUENCE_COLUMN_WIDTH } from '../constants/grid';
|
||||
import { getCellValueByColumn } from '../utils/cell';
|
||||
import GridUtils from '../utils/grid-utils';
|
||||
|
||||
const TableMain = ({
|
||||
table,
|
||||
visibleColumns,
|
||||
headerSettings,
|
||||
recordsIds,
|
||||
groupbys,
|
||||
groups,
|
||||
showSequenceColumn,
|
||||
noRecordsTipsText,
|
||||
hasMoreRecords,
|
||||
isLoadingMoreRecords,
|
||||
showGridFooter,
|
||||
loadMore,
|
||||
loadAll,
|
||||
recordGetterByIndex,
|
||||
recordGetterById,
|
||||
getClientCellValueDisplayString,
|
||||
...customProps
|
||||
}) => {
|
||||
|
||||
const gridUtils = useMemo(() => {
|
||||
return new GridUtils(recordsIds, {
|
||||
recordGetterByIndex,
|
||||
recordGetterById,
|
||||
});
|
||||
}, [recordsIds, recordGetterByIndex, recordGetterById]);
|
||||
|
||||
const tableId = useMemo(() => {
|
||||
return (table && table._id) || '';
|
||||
}, [table]);
|
||||
|
||||
const recordsCount = useMemo(() => {
|
||||
return recordsIds.length;
|
||||
}, [recordsIds]);
|
||||
|
||||
const hasNoRecords = useMemo(() => {
|
||||
return recordsCount === 0 && !hasMoreRecords;
|
||||
}, [recordsCount, hasMoreRecords]);
|
||||
|
||||
const tableColumns = useMemo(() => {
|
||||
return (table && table.columns) || [];
|
||||
}, [table]);
|
||||
|
||||
const sequenceColumnWidth = useMemo(() => {
|
||||
return showSequenceColumn ? SEQUENCE_COLUMN_WIDTH : 0;
|
||||
}, [showSequenceColumn]);
|
||||
|
||||
const groupbysCount = useMemo(() => {
|
||||
return groupbys.length;
|
||||
}, [groupbys]);
|
||||
|
||||
const groupOffset = useMemo(() => {
|
||||
return groupbysCount * GROUP_VIEW_OFFSET;
|
||||
}, [groupbysCount]);
|
||||
|
||||
const getCopiedRecordsAndColumnsFromRange = useCallback(({ type, copied, columns, isGroupView }) => {
|
||||
return gridUtils.getCopiedContent({ type, copied, isGroupView, columns });
|
||||
}, [gridUtils]);
|
||||
|
||||
const getInternalClientCellValueDisplayString = useCallback((record, column) => {
|
||||
if (getClientCellValueDisplayString) {
|
||||
return getClientCellValueDisplayString(record, column);
|
||||
}
|
||||
return getCellValueByColumn(record, column);
|
||||
}, [getClientCellValueDisplayString]);
|
||||
|
||||
return (
|
||||
<div className={classnames('sf-table-main-container container-fluid p-0', { [`group-level-${groupbysCount + 1}`]: groupbysCount > 0 })}>
|
||||
{hasNoRecords && <EmptyTip text={noRecordsTipsText || gettext('No record')} />}
|
||||
{!hasNoRecords &&
|
||||
<Records
|
||||
{...customProps}
|
||||
tableId={tableId}
|
||||
tableColumns={tableColumns}
|
||||
columns={visibleColumns}
|
||||
headerSettings={headerSettings}
|
||||
recordIds={recordsIds}
|
||||
recordsCount={recordsCount}
|
||||
groupbys={groupbys}
|
||||
groups={groups}
|
||||
groupOffsetLeft={groupOffset}
|
||||
showSequenceColumn={showSequenceColumn}
|
||||
sequenceColumnWidth={sequenceColumnWidth}
|
||||
hasMoreRecords={hasMoreRecords}
|
||||
showGridFooter={showGridFooter}
|
||||
scrollToLoadMore={loadMore}
|
||||
loadAll={loadAll}
|
||||
recordGetterById={recordGetterById}
|
||||
recordGetterByIndex={recordGetterByIndex}
|
||||
getClientCellValueDisplayString={getInternalClientCellValueDisplayString}
|
||||
getCopiedRecordsAndColumnsFromRange={getCopiedRecordsAndColumnsFromRange}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
TableMain.propTypes = {
|
||||
table: PropTypes.object,
|
||||
tableColumns: PropTypes.array,
|
||||
visibleColumns: PropTypes.array,
|
||||
showSequenceColumn: PropTypes.bool,
|
||||
recordsIds: PropTypes.array,
|
||||
groupbys: PropTypes.array,
|
||||
groups: PropTypes.array,
|
||||
noRecordsTipsText: PropTypes.string,
|
||||
modifyRecords: PropTypes.func,
|
||||
loadMore: PropTypes.func,
|
||||
loadAll: PropTypes.func,
|
||||
};
|
||||
|
||||
export default TableMain;
|
27
frontend/src/components/sf-table/table-main/load-all-tip.js
Normal file
27
frontend/src/components/sf-table/table-main/load-all-tip.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import toaster from '../../toast';
|
||||
import { gettext } from '../../../utils/constants';
|
||||
|
||||
class LoadAllTip extends React.Component {
|
||||
|
||||
onClick = () => {
|
||||
toaster.closeAll();
|
||||
this.props.load(100000);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="load-all-tip">
|
||||
<span>{gettext('Loaded 50,000 records.')}</span>
|
||||
<div className="load-all ml-2" onClick={this.onClick}>{gettext('Click to load more')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoadAllTip.propTypes = {
|
||||
load: PropTypes.func
|
||||
};
|
||||
|
||||
export default LoadAllTip;
|
@@ -0,0 +1,105 @@
|
||||
.sf-table-footer {
|
||||
position: relative;
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
line-height: 32px;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sf-table-footer.at-border {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sf-table-footer .rows-record {
|
||||
width: 80px;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.load-all-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sf-table-footer .summaries-pane {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sf-table-footer .summaries-scroll {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sf-table-footer .summaries-scroll > div {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.sf-table-footer .summary-item,
|
||||
.canvas-groups-rows .summary-item {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
padding: 0 8px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.sf-table-footer .summary-item .summary-value,
|
||||
.canvas-groups-rows .summary-item .summary-value {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
max-width: calc(100% - 18px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sf-table-footer .summary-value .summary-value-title,
|
||||
.canvas-groups-rows .summary-value .summary-value-title {
|
||||
color: #666666;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.sf-table-footer .summary-value .summary-value-text,
|
||||
.canvas-groups-rows .summary-value .summary-value-text {
|
||||
max-width: 100%;
|
||||
padding-left: 3px;
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.load-all-tip .load-all,
|
||||
.sf-table-footer .load-all {
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #666;
|
||||
}
|
||||
|
||||
.sf-table-footer .load-all {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.load-all-tip .load-all:hover,
|
||||
.sf-table-footer .load-all:hover {
|
||||
color: #212529;
|
||||
border-bottom: 1px solid #212529;
|
||||
}
|
||||
|
||||
.sf-table-footer .loading-message {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.sf-table-footer .loading-message .loading-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
@@ -0,0 +1,183 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Loading } from '@seafile/sf-metadata-ui-component';
|
||||
import toaster from '../../../toast';
|
||||
import LoadAllTip from '../load-all-tip';
|
||||
import RecordMetrics from '../../utils/record-metrics';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
import { CANVAS_RIGHT_INTERVAL } from '../../constants/grid';
|
||||
import { GRID_FOOTER as Z_INDEX_GRID_FOOTER } from '../../constants/z-index';
|
||||
import { addClassName, removeClassName } from '../../utils';
|
||||
import { getRecordsFromSelectedRange } from '../../utils/selected-cell-utils';
|
||||
|
||||
import './index.css';
|
||||
|
||||
class RecordsFooter extends React.Component {
|
||||
|
||||
ref = null;
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.calculateAtBorder);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.calculateAtBorder);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.calculateAtBorder();
|
||||
}
|
||||
|
||||
calculateAtBorder = () => {
|
||||
const { bottom } = this.ref.getBoundingClientRect();
|
||||
|
||||
// update classnames after records count change
|
||||
const originClassName = this.ref ? this.ref.className : '';
|
||||
let newClassName;
|
||||
if (bottom >= window.innerHeight) {
|
||||
newClassName = addClassName(originClassName, 'at-border');
|
||||
} else {
|
||||
newClassName = removeClassName(originClassName, 'at-border');
|
||||
}
|
||||
if (newClassName !== originClassName && this.ref) {
|
||||
this.ref.className = newClassName;
|
||||
}
|
||||
};
|
||||
|
||||
onLoadAll = () => {
|
||||
if (this.props.isLoadingMoreRecords) {
|
||||
return;
|
||||
}
|
||||
const loadNumber = this.props.recordsCount < 50000 ? 50000 : 100000;
|
||||
this.props.loadAll(loadNumber, (hasMoreRecords) => {
|
||||
if (hasMoreRecords) {
|
||||
toaster.success(<LoadAllTip load={this.props.loadAll} />, { duration: 5 });
|
||||
} else {
|
||||
toaster.success(gettext('All records loaded'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setSummaryScrollLeft = (scrollLeft) => {
|
||||
this.summaryItemsRef.scrollLeft = scrollLeft;
|
||||
};
|
||||
|
||||
getSelectedCellsCount = (selectedRange) => {
|
||||
const { topLeft, bottomRight } = selectedRange || {};
|
||||
|
||||
// if no cell selected topLeft.rowIdx is -1 , then return 0
|
||||
if (!topLeft || topLeft.rowIdx === -1) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (bottomRight.idx - topLeft.idx + 1) * (bottomRight.rowIdx - topLeft.rowIdx + 1);
|
||||
};
|
||||
|
||||
getSummaries = () => {
|
||||
const {
|
||||
isGroupView, hasSelectedRecord, recordMetrics, selectedRange, summaries,
|
||||
recordGetterByIndex,
|
||||
} = this.props;
|
||||
if (hasSelectedRecord) {
|
||||
const selectedRecordIds = RecordMetrics.getSelectedIds(recordMetrics);
|
||||
const selectedRecords = selectedRecordIds && selectedRecordIds.map(id => this.props.recordGetterById(id)).filter(Boolean);
|
||||
return this.props.getRecordsSummaries(selectedRecords);
|
||||
}
|
||||
|
||||
const selectedCellsCount = this.getSelectedCellsCount(selectedRange);
|
||||
if (selectedCellsCount > 1) {
|
||||
const records = getRecordsFromSelectedRange({ selectedRange, isGroupView, recordGetterByIndex });
|
||||
return this.props.getRecordsSummaries(records);
|
||||
}
|
||||
|
||||
return summaries;
|
||||
};
|
||||
|
||||
getSummaryItems = () => {
|
||||
const { columns, sequenceColumnWidth, hasMoreRecords, isLoadingMoreRecords } = this.props;
|
||||
const displayColumns = isLoadingMoreRecords || hasMoreRecords ? columns.slice(1, columns.length) : columns;
|
||||
let totalWidth = sequenceColumnWidth;
|
||||
let summaryItems = Array.isArray(displayColumns) && displayColumns.map((column, columnIndex) => {
|
||||
let summaryItem;
|
||||
let { width, key } = column;
|
||||
totalWidth += width;
|
||||
summaryItem = <div className="summary-item" style={{ width }} key={key}></div>;
|
||||
return summaryItem;
|
||||
});
|
||||
return { summaryItems, totalWidth };
|
||||
};
|
||||
|
||||
getRecord = () => {
|
||||
const { hasMoreRecords, hasSelectedRecord, recordMetrics, selectedRange, recordsCount } = this.props;
|
||||
if (hasSelectedRecord) {
|
||||
const selectedRecordsCount = RecordMetrics.getSelectedIds(recordMetrics).length;
|
||||
return selectedRecordsCount > 1 ? gettext('xxx records selected').replace('xxx', selectedRecordsCount) : gettext('1 record selected');
|
||||
}
|
||||
const selectedCellsCount = this.getSelectedCellsCount(selectedRange);
|
||||
if (selectedCellsCount > 1) {
|
||||
return gettext('xxx cells selected').replace('xxx', selectedCellsCount);
|
||||
}
|
||||
|
||||
let recordsCountText;
|
||||
if (recordsCount > 1) {
|
||||
recordsCountText = gettext('xxx records').replace('xxx', recordsCount);
|
||||
} else {
|
||||
recordsCountText = gettext('xxx record').replace('xxx', recordsCount);
|
||||
}
|
||||
if (hasMoreRecords) {
|
||||
recordsCountText += ' +';
|
||||
}
|
||||
return recordsCountText;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { hasMoreRecords, isLoadingMoreRecords, columns, sequenceColumnWidth, groupOffsetLeft } = this.props;
|
||||
let { summaryItems, totalWidth } = this.getSummaryItems();
|
||||
const recordWidth = (isLoadingMoreRecords || hasMoreRecords ? sequenceColumnWidth + columns[0].width : sequenceColumnWidth) + groupOffsetLeft;
|
||||
|
||||
return (
|
||||
<div className="sf-table-footer" style={{ zIndex: Z_INDEX_GRID_FOOTER }} ref={ref => this.ref = ref}>
|
||||
<div className="rows-record d-flex text-nowrap" style={{ width: recordWidth }}>
|
||||
<span>{this.getRecord()}</span>
|
||||
{(!isLoadingMoreRecords && hasMoreRecords && this.props.loadAll) &&
|
||||
<span className="load-all ml-4" onClick={this.onLoadAll}>{gettext('Load all')}</span>
|
||||
}
|
||||
{isLoadingMoreRecords &&
|
||||
<span className="loading-message ml-4">
|
||||
<span className="mr-2">{gettext('Loading')}</span>
|
||||
<Loading />
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div className="summaries-pane">
|
||||
<div className="summaries-scroll" ref={ref => this.summaryItemsRef = ref}>
|
||||
<div style={{ width: totalWidth + CANVAS_RIGHT_INTERVAL }}>
|
||||
{summaryItems || ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RecordsFooter.propTypes = {
|
||||
hasMoreRecords: PropTypes.bool,
|
||||
isLoadingMoreRecords: PropTypes.bool,
|
||||
isGroupView: PropTypes.bool,
|
||||
hasSelectedRecord: PropTypes.bool,
|
||||
recordsCount: PropTypes.number,
|
||||
summaries: PropTypes.object,
|
||||
summaryConfigs: PropTypes.object,
|
||||
columns: PropTypes.array,
|
||||
sequenceColumnWidth: PropTypes.number,
|
||||
groupOffsetLeft: PropTypes.number,
|
||||
recordMetrics: PropTypes.object,
|
||||
selectedRange: PropTypes.object,
|
||||
recordGetterById: PropTypes.func,
|
||||
recordGetterByIndex: PropTypes.func,
|
||||
getRecordsSummaries: PropTypes.func,
|
||||
loadAll: PropTypes.func,
|
||||
};
|
||||
|
||||
export default RecordsFooter;
|
@@ -0,0 +1,47 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import SelectAll from './select-all';
|
||||
import { SEQUENCE_COLUMN_WIDTH } from '../../constants/grid';
|
||||
|
||||
class ActionsCell extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
isMobile, hasSelectedRecord, isSelectedAll, isLastFrozenCell, groupOffsetLeft, height
|
||||
} = this.props;
|
||||
const columnCellClass = 'sf-table-cell column';
|
||||
const columnCellStyle = {
|
||||
height,
|
||||
width: SEQUENCE_COLUMN_WIDTH + groupOffsetLeft,
|
||||
minWidth: SEQUENCE_COLUMN_WIDTH + groupOffsetLeft,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={classnames(columnCellClass, { 'table-last--frozen': isLastFrozenCell })}
|
||||
style={{ ...columnCellStyle, backgroundColor: '#f9f9f9' }}
|
||||
>
|
||||
<SelectAll
|
||||
isMobile={isMobile}
|
||||
hasSelectedRecord={hasSelectedRecord}
|
||||
isSelectedAll={isSelectedAll}
|
||||
selectNoneRecords={this.props.selectNoneRecords}
|
||||
selectAllRecords={this.props.selectAllRecords}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ActionsCell.propTypes = {
|
||||
isMobile: PropTypes.bool,
|
||||
hasSelectedRecord: PropTypes.bool,
|
||||
isSelectedAll: PropTypes.bool,
|
||||
isLastFrozenCell: PropTypes.bool,
|
||||
height: PropTypes.number,
|
||||
groupOffsetLeft: PropTypes.number,
|
||||
selectNoneRecords: PropTypes.func,
|
||||
selectAllRecords: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionsCell;
|
@@ -0,0 +1,45 @@
|
||||
.sf-table-header-cell .sf-table-column-icon {
|
||||
fill: #aaa;
|
||||
}
|
||||
|
||||
.sf-table-header-cell .sf-metadata-icon-drop-down {
|
||||
fill: #aaa;
|
||||
font-size: 12px;
|
||||
transform: scale(.8);
|
||||
}
|
||||
|
||||
.sf-table-header-cell .sf-table-cell.column.name-column {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.sf-table-header-cell .rdg-can-drop > .sf-table-cell.column {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.sf-table-header-cell .rdg-dropping > .sf-table-cell.column {
|
||||
background: #ececec;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sf-table-header-cell .rdg-dropping-position > .sf-table-cell.column::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
height: 80%;
|
||||
width: 1px;
|
||||
background-color: #2d7ff9;
|
||||
border-radius: 50%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sf-table-header-cell .rdg-dropping-position-left > .sf-table-cell.column::before {
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.sf-table-header-cell .rdg-dropping-position-right > .sf-table-cell.column::before {
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
.sf-table-header-cell .rdg-dropping-position-none > .sf-table-cell.column::before {
|
||||
display: none;
|
||||
}
|
@@ -0,0 +1,232 @@
|
||||
import React, { useRef, useCallback, useMemo, isValidElement, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { UncontrolledTooltip } from 'reactstrap';
|
||||
import { Icon } from '@seafile/sf-metadata-ui-component';
|
||||
import ResizeColumnHandle from '../resize-column-handle';
|
||||
import HeaderDropdownMenu from '../dropdown-menu';
|
||||
import EventBus from '../../../../common/event-bus';
|
||||
import { EVENT_BUS_TYPE } from '../../../constants/event-bus-type';
|
||||
import { checkIsNameColumn } from '../../../utils/column';
|
||||
|
||||
import './index.css';
|
||||
import { MIN_COLUMN_WIDTH } from '../../../constants/grid';
|
||||
|
||||
const Cell = ({
|
||||
frozen,
|
||||
moveable,
|
||||
resizable,
|
||||
groupOffsetLeft,
|
||||
isLastFrozenCell,
|
||||
height,
|
||||
ColumnDropdownMenu,
|
||||
column,
|
||||
columnIndex,
|
||||
style: propsStyle,
|
||||
draggingColumnKey,
|
||||
draggingColumnIndex,
|
||||
dragOverColumnKey,
|
||||
frozenColumnsWidth,
|
||||
modifyLocalColumnWidth,
|
||||
modifyColumnWidth,
|
||||
onMove,
|
||||
updateDraggingKey,
|
||||
updateDragOverKey,
|
||||
}) => {
|
||||
const [disableDragColumn, setDisableDragColumn] = useState(false);
|
||||
const headerCellRef = useRef(null);
|
||||
const style = useMemo(() => {
|
||||
const { left, width } = column;
|
||||
let value = Object.assign({ width, maxWidth: width, minWidth: width, height }, propsStyle);
|
||||
if (!frozen) {
|
||||
value.left = left + groupOffsetLeft;
|
||||
}
|
||||
return value;
|
||||
}, [frozen, groupOffsetLeft, column, height, propsStyle]);
|
||||
|
||||
const getWidthFromMouseEvent = useCallback((e) => {
|
||||
let right = e.pageX || (e.touches && e.touches[0] && e.touches[0].pageX) || (e.changedTouches && e.changedTouches[e.changedTouches.length - 1].pageX);
|
||||
if (e.pageX === 0) {
|
||||
right = 0;
|
||||
}
|
||||
const left = headerCellRef.current.getBoundingClientRect().left;
|
||||
return right - left;
|
||||
}, []);
|
||||
|
||||
const onDraggingColumnWidth = useCallback((e) => {
|
||||
const width = getWidthFromMouseEvent(e);
|
||||
if (width > 0) {
|
||||
modifyLocalColumnWidth(column, width);
|
||||
}
|
||||
}, [column, getWidthFromMouseEvent, modifyLocalColumnWidth]);
|
||||
|
||||
const handleDragEndColumnWidth = useCallback((e) => {
|
||||
const width = getWidthFromMouseEvent(e);
|
||||
if (width > 0) {
|
||||
modifyColumnWidth(column, Math.max(width, MIN_COLUMN_WIDTH));
|
||||
}
|
||||
}, [column, getWidthFromMouseEvent, modifyColumnWidth]);
|
||||
|
||||
const handleHeaderCellClick = useCallback((column) => {
|
||||
const eventBus = EventBus.getInstance();
|
||||
eventBus.dispatch(EVENT_BUS_TYPE.SELECT_COLUMN, column);
|
||||
}, []);
|
||||
|
||||
const onContextMenu = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const onDragStart = useCallback((event) => {
|
||||
if (disableDragColumn) return false;
|
||||
const dragData = JSON.stringify({ type: 'sf-table-header-order', column_key: column.key, column });
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('application/drag-sf-table-header-order', dragData);
|
||||
updateDraggingKey(column.key);
|
||||
}, [column, disableDragColumn, updateDraggingKey]);
|
||||
|
||||
const onDragEnter = useCallback(() => {
|
||||
if (!draggingColumnKey) return;
|
||||
updateDragOverKey(column.key);
|
||||
}, [column, updateDragOverKey, draggingColumnKey]);
|
||||
|
||||
const onDragLeave = useCallback(() => {
|
||||
if (!draggingColumnKey) return;
|
||||
updateDragOverKey(null);
|
||||
}, [updateDragOverKey, draggingColumnKey]);
|
||||
|
||||
const onDragOver = useCallback((event) => {
|
||||
if (!draggingColumnKey) return;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
updateDragOverKey(column.key);
|
||||
if (!window.sfTableBody) return;
|
||||
let defaultColumnWidth = 200;
|
||||
const offsetX = event.clientX;
|
||||
const width = document.querySelector('.sf-table-wrapper')?.clientWidth;
|
||||
const left = window.innerWidth - width;
|
||||
if (width <= 800) {
|
||||
defaultColumnWidth = 20;
|
||||
}
|
||||
if (offsetX > window.innerWidth - defaultColumnWidth) {
|
||||
window.sfTableBody.scrollToRight();
|
||||
} else if (offsetX < frozenColumnsWidth + defaultColumnWidth + left) {
|
||||
window.sfTableBody.scrollToLeft();
|
||||
} else {
|
||||
window.sfTableBody.clearHorizontalScroll();
|
||||
}
|
||||
}, [column, frozenColumnsWidth, updateDragOverKey, draggingColumnKey]);
|
||||
|
||||
const onDrop = useCallback((event) => {
|
||||
if (!disableDragColumn) {
|
||||
event.stopPropagation();
|
||||
let dragData = event.dataTransfer.getData('application/drag-sf-table-header-order');
|
||||
if (!dragData) return false;
|
||||
dragData = JSON.parse(dragData);
|
||||
if (dragData.type !== 'sf-table-header-order' || !dragData.column_key) return false;
|
||||
if (dragData.column_key !== column.key && dragData.column.frozen === column.frozen) {
|
||||
onMove && onMove({ key: dragData.column_key }, { key: column.key });
|
||||
}
|
||||
}
|
||||
}, [column, onMove, disableDragColumn]);
|
||||
|
||||
const onDragEnd = useCallback(() => {
|
||||
updateDraggingKey(null);
|
||||
updateDragOverKey(null);
|
||||
window.sfTableBody.clearHorizontalScroll();
|
||||
}, [updateDraggingKey, updateDragOverKey]);
|
||||
|
||||
const dragDropEvents = useMemo(() => {
|
||||
if (!moveable) return {};
|
||||
return {
|
||||
onDragStart,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
onDragOver,
|
||||
onDrop,
|
||||
onDragEnd,
|
||||
};
|
||||
}, [moveable, onDragStart, onDragEnter, onDragLeave, onDragOver, onDrop, onDragEnd]);
|
||||
|
||||
const { key, display_name, icon_name, icon_tooltip } = column;
|
||||
const isNameColumn = checkIsNameColumn(column);
|
||||
const cell = (
|
||||
<div
|
||||
className={classnames('sf-table-cell column', { 'table-last--frozen': isLastFrozenCell, 'name-column': isNameColumn })}
|
||||
ref={headerCellRef}
|
||||
style={style}
|
||||
id={`sf-metadata-column-${key}`}
|
||||
onClick={() => handleHeaderCellClick(column, frozen)}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
<div className="sf-table-column-content sf-table-header-cell-left d-flex align-items-center text-truncate">
|
||||
<span className="mr-2" id={`header-icon-${key}`}>
|
||||
{icon_name && <Icon iconName={icon_name} className="sf-table-column-icon" />}
|
||||
</span>
|
||||
{icon_tooltip &&
|
||||
<UncontrolledTooltip placement="bottom" target={`header-icon-${key}`} fade={false} trigger="hover" className="sf-table-tooltip">
|
||||
{icon_tooltip}
|
||||
</UncontrolledTooltip>
|
||||
}
|
||||
<div className="header-name d-flex">
|
||||
<span title={display_name} className={classnames('header-name-text', { 'double': height === 56 })}>{display_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
{isValidElement(ColumnDropdownMenu) && <HeaderDropdownMenu ColumnDropdownMenu={ColumnDropdownMenu} column={column} setDisableDragColumn={setDisableDragColumn} />}
|
||||
{resizable && <ResizeColumnHandle onDrag={onDraggingColumnWidth} onDragEnd={handleDragEndColumnWidth} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!moveable || isNameColumn) {
|
||||
return (
|
||||
<div key={key} className="sf-table-header-cell">
|
||||
{cell}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isOver = dragOverColumnKey === column.key;
|
||||
|
||||
return (
|
||||
<div key={key} className="sf-table-header-cell">
|
||||
<div
|
||||
draggable="true"
|
||||
style={{ opacity: draggingColumnKey === column.key ? 0.2 : 1 }}
|
||||
className={classnames('rdg-can-drop', {
|
||||
'rdg-dropping rdg-dropping-position': isOver,
|
||||
'rdg-dropping-position-left': isOver && draggingColumnIndex > columnIndex,
|
||||
'rdg-dropping-position-right': isOver && draggingColumnIndex < columnIndex,
|
||||
'rdg-dropping-position-none': isOver && draggingColumnIndex === columnIndex
|
||||
})}
|
||||
{...dragDropEvents}
|
||||
>
|
||||
{cell}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Cell.defaultProps = {
|
||||
style: null,
|
||||
};
|
||||
|
||||
Cell.propTypes = {
|
||||
groupOffsetLeft: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
column: PropTypes.object,
|
||||
ColumnDropdownMenu: PropTypes.object,
|
||||
columnIndex: PropTypes.number,
|
||||
style: PropTypes.object,
|
||||
frozen: PropTypes.bool,
|
||||
moveable: PropTypes.bool,
|
||||
resizable: PropTypes.bool,
|
||||
isLastFrozenCell: PropTypes.bool,
|
||||
draggingColumnKey: PropTypes.string,
|
||||
draggingColumnIndex: PropTypes.number,
|
||||
dragOverColumnKey: PropTypes.string,
|
||||
modifyLocalColumnWidth: PropTypes.func,
|
||||
updateDraggingKey: PropTypes.func,
|
||||
updateDragOverKey: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Cell;
|
@@ -0,0 +1,72 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { UncontrolledTooltip, DropdownItem } from 'reactstrap';
|
||||
import classnames from 'classnames';
|
||||
import { Icon } from '@seafile/sf-metadata-ui-component';
|
||||
|
||||
const ColumnDropdownItem = ({ disabled, iconName, target, title, tip, className, onChange, onMouseEnter }) => {
|
||||
const [isShowToolTip, setToolTipShow] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
setToolTipShow(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onClick = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
event.nativeEvent.stopImmediatePropagation();
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
if (!disabled) {
|
||||
return (
|
||||
<DropdownItem id={target} onClick={onChange} onMouseEnter={onMouseEnter} className={className}>
|
||||
<Icon iconName={iconName} />
|
||||
<span className="item-text">{title}</span>
|
||||
</DropdownItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownItem
|
||||
className={classnames('disabled', className)}
|
||||
toggle={true}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
id={target}
|
||||
>
|
||||
<Icon iconName={iconName} />
|
||||
<span className="item-text">{title}</span>
|
||||
{isShowToolTip && (
|
||||
<UncontrolledTooltip placement="right" target={target} fade={false} delay={{ show: 0, hide: 0 }} className="sf-table-tooltip">
|
||||
{tip}
|
||||
</UncontrolledTooltip>
|
||||
)}
|
||||
</DropdownItem>
|
||||
</>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
ColumnDropdownItem.propTypes = {
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
target: PropTypes.string.isRequired,
|
||||
iconName: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
tip: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ColumnDropdownItem.defaultProps = {
|
||||
onChange: () => {},
|
||||
onMouseEnter: () => {},
|
||||
disabled: false,
|
||||
className: '',
|
||||
};
|
||||
|
||||
export default ColumnDropdownItem;
|
@@ -0,0 +1,37 @@
|
||||
.sf-table-dropdown-menu .dropdown-item .sf-metadata-icon {
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.sf-table-dropdown-menu .dropdown-item:hover .sf-metadata-icon {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.sf-table-dropdown-menu .sf-metadata-column-dropdown-item .item-text {
|
||||
width: calc(100% - 30px);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.sf-table-dropdown-menu .dropdown-toggle::after {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.sf-table-dropdown-menu .dropdown-toggle:hover::after {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sf-table-dropdown-menu .dropdown-item.disabled,
|
||||
.sf-table-dropdown-menu .dropdown-item:disabled {
|
||||
pointer-events: unset !important;
|
||||
}
|
||||
|
||||
.sf-table-dropdown-menu .disabled.dropdown-item:hover {
|
||||
background-color: unset;
|
||||
cursor: default;
|
||||
color: #c2c2c2;
|
||||
}
|
||||
|
||||
.sf-table-dropdown-menu .disabled.dropdown-item .sf-metadata-icon {
|
||||
fill: #c2c2c2;
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
import React, { useState, useCallback, cloneElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Dropdown, DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||
import { ModalPortal, Icon } from '@seafile/sf-metadata-ui-component';
|
||||
import { gettext } from '../../../../../utils/constants';
|
||||
import { isMobile } from '../../../../../utils/utils';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const HeaderDropdownMenu = ({ column, ColumnDropdownMenu, customProps }) => {
|
||||
const [isMenuShow, setMenuShow] = useState(false);
|
||||
|
||||
const onToggle = useCallback((event) => {
|
||||
event && event.preventDefault();
|
||||
event && event.stopPropagation();
|
||||
const targetDom = event.target;
|
||||
if (targetDom.className === 'string' && targetDom.className.includes('disabled')) return;
|
||||
setMenuShow(!isMenuShow);
|
||||
}, [isMenuShow]);
|
||||
|
||||
const renderDropdownMenu = useCallback(() => {
|
||||
return (
|
||||
<DropdownMenu
|
||||
positionFixed
|
||||
flip={false}
|
||||
modifiers={{ preventOverflow: { boundariesElement: document.body } }}
|
||||
className="sf-table-dropdown-menu"
|
||||
>
|
||||
{cloneElement(ColumnDropdownMenu, { column, ...customProps })}
|
||||
</DropdownMenu>
|
||||
);
|
||||
}, [ColumnDropdownMenu, column, customProps]);
|
||||
|
||||
return (
|
||||
<Dropdown direction="down" className="sf-table-dropdown" isOpen={isMenuShow} toggle={onToggle}>
|
||||
<DropdownToggle
|
||||
tag="span"
|
||||
role="button"
|
||||
data-toggle="dropdown"
|
||||
aria-expanded={isMenuShow}
|
||||
title={gettext('More operations')}
|
||||
aria-label={gettext('More operations')}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon iconName="drop-down" />
|
||||
</DropdownToggle>
|
||||
{isMenuShow && !isMobile &&
|
||||
<ModalPortal>
|
||||
<div className="sf-table-dropdown-menu-wrapper large">{renderDropdownMenu()}</div>
|
||||
</ModalPortal>
|
||||
}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
HeaderDropdownMenu.propTypes = {
|
||||
column: PropTypes.object.isRequired,
|
||||
ColumnDropdownMenu: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default HeaderDropdownMenu;
|
@@ -0,0 +1,214 @@
|
||||
import React, { useCallback, useMemo, useState, isValidElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Cell from './cell';
|
||||
import ActionsCell from './actions-cell';
|
||||
import InsertColumn from './insert-column';
|
||||
import { GRID_HEADER as Z_INDEX_GRID_HEADER, SEQUENCE_COLUMN as Z_INDEX_SEQUENCE_COLUMN } from '../../constants/z-index';
|
||||
import { HEADER_HEIGHT_TYPE, GRID_HEADER_DEFAULT_HEIGHT, GRID_HEADER_DOUBLE_HEIGHT, MIN_COLUMN_WIDTH } from '../../constants/grid';
|
||||
import { isEmptyObject } from '../../../../utils/object';
|
||||
import { getFrozenColumns, checkIsColumnFrozen, recalculateColumnMetricsByResizeColumn } from '../../utils/column';
|
||||
import { isMobile } from '../../../../utils/utils';
|
||||
|
||||
const RecordsHeader = ({
|
||||
containerWidth,
|
||||
showSequenceColumn,
|
||||
sequenceColumnWidth,
|
||||
isGroupView,
|
||||
hasSelectedRecord,
|
||||
isSelectedAll,
|
||||
lastFrozenColumnKey,
|
||||
groupOffsetLeft,
|
||||
ColumnDropdownMenu,
|
||||
NewColumnComponent,
|
||||
headerSettings,
|
||||
columnMetrics: propsColumnMetrics,
|
||||
colOverScanStartIdx,
|
||||
colOverScanEndIdx,
|
||||
onRef,
|
||||
selectNoneRecords,
|
||||
selectAllRecords,
|
||||
modifyColumnWidth: modifyColumnWidthAPI,
|
||||
modifyColumnOrder: modifyColumnOrderAPI,
|
||||
insertColumn,
|
||||
...customProps
|
||||
}) => {
|
||||
const [resizingColumnMetrics, setResizingColumnMetrics] = useState(null);
|
||||
const [draggingColumnKey, setDraggingCellKey] = useState(null);
|
||||
const [dragOverColumnKey, setDragOverCellKey] = useState(null);
|
||||
|
||||
const height = useMemo(() => {
|
||||
const heightMode = isEmptyObject(headerSettings) ? HEADER_HEIGHT_TYPE.DEFAULT : headerSettings.header_height;
|
||||
return heightMode === HEADER_HEIGHT_TYPE.DOUBLE ? GRID_HEADER_DOUBLE_HEIGHT : GRID_HEADER_DEFAULT_HEIGHT;
|
||||
}, [headerSettings]);
|
||||
|
||||
const rowStyle = useMemo(() => {
|
||||
return {
|
||||
width: containerWidth,
|
||||
minWidth: '100%',
|
||||
zIndex: Z_INDEX_GRID_HEADER,
|
||||
height,
|
||||
};
|
||||
}, [containerWidth, height]);
|
||||
|
||||
const columnMetrics = useMemo(() => {
|
||||
if (resizingColumnMetrics) return resizingColumnMetrics;
|
||||
return propsColumnMetrics;
|
||||
}, [resizingColumnMetrics, propsColumnMetrics]);
|
||||
|
||||
const wrapperStyle = useMemo(() => {
|
||||
const { columns } = columnMetrics;
|
||||
let value = {
|
||||
position: (isMobile ? 'absolute' : 'fixed'),
|
||||
marginLeft: '0px',
|
||||
height,
|
||||
zIndex: Z_INDEX_SEQUENCE_COLUMN,
|
||||
};
|
||||
if ((isGroupView && !checkIsColumnFrozen(columns[0])) || isMobile) {
|
||||
value.position = 'absolute';
|
||||
}
|
||||
return value;
|
||||
}, [isGroupView, columnMetrics, height]);
|
||||
|
||||
const moveable = useMemo(() => {
|
||||
return !!modifyColumnOrderAPI;
|
||||
}, [modifyColumnOrderAPI]);
|
||||
|
||||
const resizable = useMemo(() => {
|
||||
return !!modifyColumnWidthAPI;
|
||||
}, [modifyColumnWidthAPI]);
|
||||
|
||||
const modifyLocalColumnWidth = useCallback((column, width) => {
|
||||
setResizingColumnMetrics(recalculateColumnMetricsByResizeColumn(propsColumnMetrics, sequenceColumnWidth, column.key, Math.max(width, MIN_COLUMN_WIDTH)));
|
||||
}, [propsColumnMetrics, sequenceColumnWidth]);
|
||||
|
||||
const modifyColumnWidth = useCallback((column, newWidth) => {
|
||||
setResizingColumnMetrics(null);
|
||||
modifyColumnWidthAPI && modifyColumnWidthAPI(column, newWidth);
|
||||
}, [modifyColumnWidthAPI]);
|
||||
|
||||
const modifyColumnOrder = useCallback((source, target) => {
|
||||
modifyColumnOrderAPI && modifyColumnOrderAPI(source.key, target.key);
|
||||
}, [modifyColumnOrderAPI]);
|
||||
|
||||
const updateDraggingKey = useCallback((cellKey) => {
|
||||
if (cellKey === draggingColumnKey) return;
|
||||
setDraggingCellKey(cellKey);
|
||||
}, [draggingColumnKey]);
|
||||
|
||||
const updateDragOverKey = useCallback((cellKey) => {
|
||||
if (cellKey === dragOverColumnKey) return;
|
||||
setDragOverCellKey(cellKey);
|
||||
}, [dragOverColumnKey]);
|
||||
|
||||
const frozenColumns = getFrozenColumns(columnMetrics.columns);
|
||||
const displayColumns = columnMetrics.columns.slice(colOverScanStartIdx, colOverScanEndIdx);
|
||||
const frozenColumnsWidth = frozenColumns.reduce((total, c) => total + c.width, groupOffsetLeft + sequenceColumnWidth);
|
||||
const draggingColumnIndex = draggingColumnKey ? columnMetrics.columns.findIndex(c => c.key === draggingColumnKey) : -1;
|
||||
|
||||
return (
|
||||
<div className="static-sf-table-result-content grid-header" style={{ height: height + 1 }}>
|
||||
<div className="sf-table-row" style={rowStyle}>
|
||||
{/* frozen */}
|
||||
<div className="frozen-columns d-flex" style={wrapperStyle} ref={ref => onRef(ref)}>
|
||||
{showSequenceColumn &&
|
||||
<ActionsCell
|
||||
isMobile={isMobile}
|
||||
height={height}
|
||||
hasSelectedRecord={hasSelectedRecord}
|
||||
isSelectedAll={isSelectedAll}
|
||||
isLastFrozenCell={!lastFrozenColumnKey}
|
||||
groupOffsetLeft={groupOffsetLeft}
|
||||
selectNoneRecords={selectNoneRecords}
|
||||
selectAllRecords={selectAllRecords}
|
||||
/>
|
||||
}
|
||||
{frozenColumns.map((column, columnIndex) => {
|
||||
const { key } = column;
|
||||
const style = { backgroundColor: '#f9f9f9' };
|
||||
const isLastFrozenCell = key === lastFrozenColumnKey;
|
||||
return (
|
||||
<Cell
|
||||
{...customProps}
|
||||
key={`sf-table-frozen-header-cell-${key}`}
|
||||
frozen
|
||||
moveable={moveable}
|
||||
resizable={resizable}
|
||||
height={height}
|
||||
column={column}
|
||||
columnIndex={columnIndex}
|
||||
groupOffsetLeft={groupOffsetLeft}
|
||||
style={style}
|
||||
isLastFrozenCell={isLastFrozenCell}
|
||||
frozenColumnsWidth={frozenColumnsWidth}
|
||||
ColumnDropdownMenu={ColumnDropdownMenu}
|
||||
draggingColumnKey={draggingColumnKey}
|
||||
draggingColumnIndex={draggingColumnIndex}
|
||||
dragOverColumnKey={dragOverColumnKey}
|
||||
modifyLocalColumnWidth={modifyLocalColumnWidth}
|
||||
modifyColumnWidth={modifyColumnWidth}
|
||||
onMove={modifyColumnOrder}
|
||||
updateDraggingKey={updateDraggingKey}
|
||||
updateDragOverKey={updateDragOverKey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* scroll */}
|
||||
{displayColumns.map((column, columnIndex) => {
|
||||
return (
|
||||
<Cell
|
||||
key={`sf-table-header-cell-${column.key}`}
|
||||
{...customProps}
|
||||
moveable={moveable}
|
||||
resizable={resizable}
|
||||
ColumnDropdownMenu={ColumnDropdownMenu}
|
||||
groupOffsetLeft={groupOffsetLeft}
|
||||
height={height}
|
||||
column={column}
|
||||
columnIndex={columnIndex}
|
||||
draggingColumnKey={draggingColumnKey}
|
||||
draggingColumnIndex={draggingColumnIndex}
|
||||
dragOverColumnKey={dragOverColumnKey}
|
||||
frozenColumnsWidth={frozenColumnsWidth}
|
||||
modifyLocalColumnWidth={modifyLocalColumnWidth}
|
||||
modifyColumnWidth={modifyColumnWidth}
|
||||
onMove={modifyColumnOrder}
|
||||
updateDraggingKey={updateDraggingKey}
|
||||
updateDragOverKey={updateDragOverKey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{(insertColumn && isValidElement(NewColumnComponent)) && (
|
||||
<InsertColumn
|
||||
lastColumn={columnMetrics.columns[columnMetrics.columns.length - 1]}
|
||||
groupOffsetLeft={groupOffsetLeft}
|
||||
height={height}
|
||||
NewColumnComponent={NewColumnComponent}
|
||||
insertColumn={insertColumn}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RecordsHeader.propTypes = {
|
||||
containerWidth: PropTypes.number,
|
||||
showSequenceColumn: PropTypes.bool,
|
||||
sequenceColumnWidth: PropTypes.number,
|
||||
columnMetrics: PropTypes.object.isRequired,
|
||||
colOverScanStartIdx: PropTypes.number,
|
||||
colOverScanEndIdx: PropTypes.number,
|
||||
enableRecordNumber: PropTypes.bool,
|
||||
hasSelectedRecord: PropTypes.bool,
|
||||
isSelectedAll: PropTypes.bool,
|
||||
isGroupView: PropTypes.bool,
|
||||
groupOffsetLeft: PropTypes.number,
|
||||
lastFrozenColumnKey: PropTypes.string,
|
||||
onRef: PropTypes.func,
|
||||
selectNoneRecords: PropTypes.func,
|
||||
selectAllRecords: PropTypes.func,
|
||||
insertColumn: PropTypes.func,
|
||||
};
|
||||
|
||||
export default RecordsHeader;
|
@@ -0,0 +1,9 @@
|
||||
.sf-table-header-cell .sf-table-cell.insert-column {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sf-table-header-cell .sf-table-cell.insert-column:hover {
|
||||
cursor: pointer;
|
||||
}
|
@@ -0,0 +1,64 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, cloneElement } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon } from '@seafile/sf-metadata-ui-component';
|
||||
import { isEnter } from '../../../../../utils/hotkey';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const InsertColumn = ({ lastColumn, height, groupOffsetLeft, NewColumnComponent, insertColumn: insertColumnAPI }) => {
|
||||
const id = useMemo(() => 'sf-table-add-column', []);
|
||||
const ref = useRef(null);
|
||||
const style = useMemo(() => {
|
||||
return {
|
||||
height: height,
|
||||
width: 44,
|
||||
minWidth: 44,
|
||||
maxWidth: 44,
|
||||
left: lastColumn.left + lastColumn.width + groupOffsetLeft,
|
||||
position: 'absolute',
|
||||
};
|
||||
}, [lastColumn, height, groupOffsetLeft]);
|
||||
|
||||
const openPopover = useCallback(() => {
|
||||
ref?.current?.click();
|
||||
}, [ref]);
|
||||
|
||||
const insertColumn = useCallback((name, type, { key, data }) => {
|
||||
insertColumnAPI(name, type, { key, data });
|
||||
}, [insertColumnAPI]);
|
||||
|
||||
const onHotKey = useCallback((event) => {
|
||||
if (isEnter(event) && document.activeElement && document.activeElement.id === id) {
|
||||
openPopover();
|
||||
}
|
||||
}, [id, openPopover]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', onHotKey);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onHotKey);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sf-table-header-cell">
|
||||
<div className="sf-table-cell column insert-column" style={style} id={id} ref={ref}>
|
||||
<Icon iconName="add-table" />
|
||||
</div>
|
||||
</div>
|
||||
{cloneElement(NewColumnComponent, { target: id, onChange: insertColumn })}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
InsertColumn.propTypes = {
|
||||
lastColumn: PropTypes.object.isRequired,
|
||||
height: PropTypes.number,
|
||||
groupOffsetLeft: PropTypes.number,
|
||||
NewColumnComponent: PropTypes.object,
|
||||
insertColumn: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default InsertColumn;
|
@@ -0,0 +1,60 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { debounce } from '../../../../utils/utils';
|
||||
|
||||
class ResizeColumnHandle extends Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
this.cleanUp();
|
||||
}
|
||||
|
||||
cleanUp = () => {
|
||||
window.removeEventListener('mouseup', this.onMouseUp);
|
||||
window.removeEventListener('mousemove', this.onMouseMove);
|
||||
window.removeEventListener('touchend', this.onMouseUp);
|
||||
window.removeEventListener('touchmove', this.onMouseMove);
|
||||
};
|
||||
|
||||
onMouseDown = (e) => {
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
window.addEventListener('mouseup', this.onMouseUp);
|
||||
window.addEventListener('mousemove', this.onMouseMove);
|
||||
window.addEventListener('touchend', this.onMouseUp);
|
||||
window.addEventListener('touchmove', this.onMouseMove);
|
||||
};
|
||||
|
||||
onMouseUp = (e) => {
|
||||
this.props.onDragEnd && this.props.onDragEnd(e);
|
||||
this.cleanUp();
|
||||
};
|
||||
|
||||
onMouseMove = (e) => {
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
debounce(this.props.onDrag(e), 100);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className="record-HeaderCell__draggable"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onDrag={this.props.onDrag}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onTouchStart={this.onMouseDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ResizeColumnHandle.propTypes = {
|
||||
onDrag: PropTypes.func,
|
||||
onDragEnd: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ResizeColumnHandle;
|
@@ -0,0 +1,101 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Icon } from '@seafile/sf-metadata-ui-component';
|
||||
import { gettext } from '../../../../utils/constants';
|
||||
|
||||
class SelectAll extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isSelectedAll: props.isSelectedAll,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { isSelectedAll } = this.props;
|
||||
if (isSelectedAll !== prevProps.isSelectedAll) {
|
||||
this.setState({
|
||||
isSelectedAll,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onToggleSelectAll = (e) => {
|
||||
const { isMobile, hasSelectedRecord } = this.props;
|
||||
const { isSelectedAll } = this.state;
|
||||
if (isMobile) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (hasSelectedRecord || isSelectedAll) {
|
||||
this.setState({ isSelectedAll: false });
|
||||
this.props.selectNoneRecords();
|
||||
return;
|
||||
}
|
||||
this.setState({ isSelectedAll: true });
|
||||
this.props.selectAllRecords();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isMobile, hasSelectedRecord } = this.props;
|
||||
const { isSelectedAll } = this.state;
|
||||
const isSelectedParts = hasSelectedRecord && !isSelectedAll;
|
||||
return (
|
||||
<div className="select-all-checkbox-container" onClick={this.onToggleSelectAll}>
|
||||
{isMobile ?
|
||||
<label className='mobile-select-all-container'>
|
||||
{isSelectedParts ?
|
||||
(<Icon iconName="partially-selected" />) :
|
||||
(
|
||||
<>
|
||||
<input
|
||||
className="mobile-select-all-checkbox"
|
||||
name="mobile-select-all-checkbox"
|
||||
type="checkbox"
|
||||
checked={isSelectedAll}
|
||||
readOnly
|
||||
/>
|
||||
<div className='select-all-checkbox-show'></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</label> :
|
||||
<>
|
||||
{isSelectedParts ?
|
||||
(<Icon iconName="partially-selected" />) :
|
||||
(
|
||||
<input
|
||||
id="select-all-checkbox"
|
||||
className="select-all-checkbox"
|
||||
type="checkbox"
|
||||
name={gettext('Select all')}
|
||||
title={gettext('Select all')}
|
||||
aria-label={gettext('Select all')}
|
||||
checked={isSelectedAll}
|
||||
readOnly
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
}
|
||||
<label
|
||||
htmlFor="select-all-checkbox"
|
||||
name={gettext('Select all')}
|
||||
title={gettext('Select all')}
|
||||
aria-label={gettext('Select all')}
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectAll.propTypes = {
|
||||
isMobile: PropTypes.bool,
|
||||
hasSelectedRecord: PropTypes.bool,
|
||||
isSelectedAll: PropTypes.bool,
|
||||
selectNoneRecords: PropTypes.func,
|
||||
selectAllRecords: PropTypes.func,
|
||||
};
|
||||
|
||||
export default SelectAll;
|
668
frontend/src/components/sf-table/table-main/records/body.js
Normal file
668
frontend/src/components/sf-table/table-main/records/body.js
Normal file
@@ -0,0 +1,668 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Loading } from '@seafile/sf-metadata-ui-component';
|
||||
import { RightScrollbar } from '../../scrollbar';
|
||||
import Record from './record';
|
||||
import InteractionMasks from '../../masks/interaction-masks';
|
||||
import RecordMetrics from '../../utils/record-metrics';
|
||||
import { getColumnScrollPosition, getColVisibleStartIdx, getColVisibleEndIdx } from '../../utils/records-body-utils';
|
||||
import EventBus from '../../../common/event-bus';
|
||||
import { EVENT_BUS_TYPE } from '../../constants/event-bus-type';
|
||||
import { isShiftKeyDown } from '../../../../utils/keyboard-utils';
|
||||
import { checkEditableViaClickCell, checkIsColumnSupportDirectEdit, getColumnByIndex, getColumnIndexByKey } from '../../utils/column';
|
||||
import { checkIsCellSupportOpenEditor } from '../../utils/selected-cell-utils';
|
||||
|
||||
const ROW_HEIGHT = 33;
|
||||
const RENDER_MORE_NUMBER = 10;
|
||||
const CONTENT_HEIGHT = window.innerHeight - 174;
|
||||
const { max, min, ceil, round } = Math;
|
||||
|
||||
class RecordsBody extends Component {
|
||||
|
||||
static defaultProps = {
|
||||
editorPortalTarget: document.body,
|
||||
scrollToRowIndex: 0,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
startRenderIndex: 0,
|
||||
endRenderIndex: this.getInitEndIndex(props),
|
||||
activeRecords: [],
|
||||
menuPosition: null,
|
||||
selectedPosition: null,
|
||||
isScrollingRightScrollbar: false,
|
||||
};
|
||||
this.eventBus = EventBus.getInstance();
|
||||
this.resultContentRef = null;
|
||||
this.resultRef = null;
|
||||
this.recordFrozenRefs = [];
|
||||
this.rowVisibleStart = 0;
|
||||
this.rowVisibleEnd = this.setRecordVisibleEnd();
|
||||
this.columnVisibleStart = 0;
|
||||
this.columnVisibleEnd = this.setColumnVisibleEnd();
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.onRef(this);
|
||||
window.sfTableBody = this;
|
||||
this.unsubscribeFocus = this.eventBus.subscribe(EVENT_BUS_TYPE.FOCUS_CANVAS, this.onFocus);
|
||||
this.unsubscribeSelectColumn = this.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_COLUMN, this.onColumnSelect);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const { recordsCount, recordIds } = nextProps;
|
||||
if (recordsCount !== this.props.recordsCount || recordIds !== this.props.recordIds) {
|
||||
this.recalculateRenderIndex(recordIds);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.storeScrollPosition();
|
||||
this.clearHorizontalScroll();
|
||||
this.clearScrollbarTimer();
|
||||
this.unsubscribeFocus();
|
||||
this.unsubscribeSelectColumn();
|
||||
window.sfTableBody = null;
|
||||
this.setState = (state, callback) => {
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
storeScrollPosition = () => {
|
||||
this.props.storeScrollPosition();
|
||||
};
|
||||
|
||||
onFocus = () => {
|
||||
if (this.interactionMask.container) {
|
||||
this.interactionMask.focus();
|
||||
return;
|
||||
}
|
||||
this.resultContentRef.focus();
|
||||
};
|
||||
|
||||
onColumnSelect = (column) => {
|
||||
const { columns } = this.props;
|
||||
const selectColumnIndex = getColumnIndexByKey(column.key, columns);
|
||||
this.setState({
|
||||
selectedPosition: { ...this.state.selectedPosition, idx: selectColumnIndex, rowIdx: 0 },
|
||||
});
|
||||
};
|
||||
|
||||
getVisibleIndex = () => {
|
||||
return { rowVisibleStartIdx: this.rowVisibleStart, rowVisibleEndIdx: this.rowVisibleEnd };
|
||||
};
|
||||
|
||||
getShownRecords = () => {
|
||||
return this.getShownRecordIds().map((id) => this.props.recordGetterById(id));
|
||||
};
|
||||
|
||||
setRecordVisibleEnd = () => {
|
||||
return max(ceil(CONTENT_HEIGHT / ROW_HEIGHT), 0);
|
||||
};
|
||||
|
||||
setColumnVisibleEnd = () => {
|
||||
const { columns, getScrollLeft, getTableContentRect } = this.props;
|
||||
const { width: tableContentWidth } = getTableContentRect();
|
||||
let columnVisibleEnd = 0;
|
||||
const contentScrollLeft = getScrollLeft();
|
||||
let endColumnWidth = tableContentWidth + contentScrollLeft;
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
const { width } = columns[i];
|
||||
endColumnWidth = endColumnWidth - width;
|
||||
if (endColumnWidth < 0) {
|
||||
return columnVisibleEnd = i;
|
||||
}
|
||||
}
|
||||
return columnVisibleEnd;
|
||||
};
|
||||
|
||||
recalculateRenderIndex = (recordIds) => {
|
||||
const { startRenderIndex, endRenderIndex } = this.state;
|
||||
const contentScrollTop = this.resultContentRef.scrollTop;
|
||||
const start = Math.max(0, Math.floor(contentScrollTop / ROW_HEIGHT) - RENDER_MORE_NUMBER);
|
||||
const { height } = this.props.getTableContentRect();
|
||||
const end = Math.min(Math.ceil((contentScrollTop + height) / ROW_HEIGHT) + RENDER_MORE_NUMBER, recordIds.length);
|
||||
if (start !== startRenderIndex) {
|
||||
this.setState({ startRenderIndex: start });
|
||||
}
|
||||
if (end !== endRenderIndex) {
|
||||
this.setState({ endRenderIndex: end });
|
||||
}
|
||||
};
|
||||
|
||||
getInitEndIndex = (props) => {
|
||||
return Math.min(Math.ceil(window.innerHeight / ROW_HEIGHT) + RENDER_MORE_NUMBER, props.recordsCount);
|
||||
};
|
||||
|
||||
getShownRecordIds = () => {
|
||||
const { recordIds } = this.props;
|
||||
const { startRenderIndex, endRenderIndex } = this.state;
|
||||
return recordIds.slice(startRenderIndex, endRenderIndex);
|
||||
};
|
||||
|
||||
getRowTop = (rowIdx) => {
|
||||
return ROW_HEIGHT * rowIdx;
|
||||
};
|
||||
|
||||
getRowHeight = () => {
|
||||
return ROW_HEIGHT;
|
||||
};
|
||||
|
||||
jumpToRow = (scrollToRowIndex) => {
|
||||
const { recordsCount } = this.props;
|
||||
const rowHeight = this.getRowHeight();
|
||||
const height = this.resultContentRef.offsetHeight;
|
||||
const scrollTop = Math.min(scrollToRowIndex * rowHeight, recordsCount * rowHeight - height);
|
||||
this.setScrollTop(scrollTop);
|
||||
};
|
||||
|
||||
scrollToColumn = (idx) => {
|
||||
const { columns, getTableContentRect } = this.props;
|
||||
const { width: tableContentWidth } = getTableContentRect();
|
||||
const newScrollLeft = getColumnScrollPosition(columns, idx, tableContentWidth);
|
||||
if (newScrollLeft !== null) {
|
||||
this.props.setRecordsScrollLeft(newScrollLeft);
|
||||
}
|
||||
this.updateColVisibleIndex(newScrollLeft);
|
||||
};
|
||||
|
||||
updateColVisibleIndex = (scrollLeft) => {
|
||||
const { columns } = this.props;
|
||||
const columnVisibleStart = getColVisibleStartIdx(columns, scrollLeft);
|
||||
const columnVisibleEnd = getColVisibleEndIdx(columns, window.innerWidth, scrollLeft);
|
||||
this.columnVisibleStart = columnVisibleStart;
|
||||
this.columnVisibleEnd = columnVisibleEnd;
|
||||
};
|
||||
|
||||
setScrollTop = (scrollTop) => {
|
||||
this.resultContentRef.scrollTop = scrollTop;
|
||||
};
|
||||
|
||||
setScrollLeft = (scrollLeft, scrollTop) => {
|
||||
const { interactionMask } = this;
|
||||
interactionMask && interactionMask.setScrollLeft(scrollLeft, scrollTop);
|
||||
};
|
||||
|
||||
cancelSetScrollLeft = () => {
|
||||
const { interactionMask } = this;
|
||||
interactionMask && interactionMask.cancelSetScrollLeft();
|
||||
};
|
||||
|
||||
getClientScrollTopOffset = (node) => {
|
||||
const rowHeight = this.getRowHeight();
|
||||
const scrollVariation = node.scrollTop % rowHeight;
|
||||
return scrollVariation > 0 ? rowHeight - scrollVariation : 0;
|
||||
};
|
||||
|
||||
onHitBottomCanvas = () => {
|
||||
const rowHeight = this.getRowHeight();
|
||||
const node = this.resultContentRef;
|
||||
node.scrollTop += rowHeight + this.getClientScrollTopOffset(node);
|
||||
};
|
||||
|
||||
onHitTopCanvas = () => {
|
||||
const rowHeight = this.getRowHeight();
|
||||
const node = this.resultContentRef;
|
||||
node.scrollTop -= (rowHeight - this.getClientScrollTopOffset(node));
|
||||
};
|
||||
|
||||
getScrollTop = () => {
|
||||
return this.resultContentRef ? this.resultContentRef.scrollTop : 0;
|
||||
};
|
||||
|
||||
getRecordBodyHeight = () => {
|
||||
return this.resultContentRef ? this.resultContentRef.offsetHeight : 0;
|
||||
};
|
||||
|
||||
onScroll = () => {
|
||||
const { recordsCount } = this.props;
|
||||
const { startRenderIndex, endRenderIndex } = this.state;
|
||||
const { offsetHeight, scrollTop: contentScrollTop } = this.resultContentRef;
|
||||
|
||||
// Calculate the start rendering row index, and end rendering row index
|
||||
const start = Math.max(0, Math.floor(contentScrollTop / ROW_HEIGHT) - RENDER_MORE_NUMBER);
|
||||
const end = Math.min(Math.ceil((contentScrollTop + this.resultContentRef.offsetHeight) / ROW_HEIGHT) + RENDER_MORE_NUMBER, recordsCount);
|
||||
|
||||
this.oldScrollTop = contentScrollTop;
|
||||
const renderedRecordsCount = ceil(this.resultContentRef.offsetHeight / ROW_HEIGHT);
|
||||
const newRecordVisibleStart = max(0, round(contentScrollTop / ROW_HEIGHT));
|
||||
const newRecordVisibleEnd = min(newRecordVisibleStart + renderedRecordsCount, recordsCount);
|
||||
this.rowVisibleStart = newRecordVisibleStart;
|
||||
this.rowVisibleEnd = newRecordVisibleEnd;
|
||||
|
||||
if (Math.abs(start - startRenderIndex) > 5 || start < 5) {
|
||||
this.setState({ startRenderIndex: start });
|
||||
}
|
||||
if (Math.abs(end - endRenderIndex) > 5 || end > recordsCount - 5) {
|
||||
this.setState({ endRenderIndex: end });
|
||||
}
|
||||
// Scroll to the bottom of the page, load more records
|
||||
if (offsetHeight + contentScrollTop >= this.resultContentRef.scrollHeight) {
|
||||
if (this.props.scrollToLoadMore) {
|
||||
this.props.scrollToLoadMore();
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isScrollingRightScrollbar) {
|
||||
this.setRightScrollbarScrollTop(this.oldScrollTop);
|
||||
}
|
||||
|
||||
// solve the bug that the scroll bar disappears when scrolling too fast
|
||||
this.clearScrollbarTimer();
|
||||
this.scrollbarTimer = setTimeout(() => {
|
||||
this.setState({ isScrollingRightScrollbar: false });
|
||||
}, 300);
|
||||
};
|
||||
|
||||
onScrollbarScroll = (scrollTop) => {
|
||||
// solve canvas&rightScrollbar circle scroll problem
|
||||
if (this.oldScrollTop === scrollTop) {
|
||||
return;
|
||||
}
|
||||
this.setState({ isScrollingRightScrollbar: true }, () => {
|
||||
this.setScrollTop(scrollTop);
|
||||
});
|
||||
};
|
||||
|
||||
onScrollbarMouseUp = () => {
|
||||
this.setState({ isScrollingRightScrollbar: false });
|
||||
};
|
||||
|
||||
setRightScrollbarScrollTop = (scrollTop) => {
|
||||
this.rightScrollbar && this.rightScrollbar.setScrollTop(scrollTop);
|
||||
};
|
||||
|
||||
selectNoneCells = () => {
|
||||
this.interactionMask && this.interactionMask.selectNone();
|
||||
const { selectedPosition } = this.state;
|
||||
if (!selectedPosition || selectedPosition.idx < 0 || selectedPosition.rowIdx < 0) {
|
||||
return;
|
||||
}
|
||||
this.selectNone();
|
||||
};
|
||||
|
||||
selectNone = () => {
|
||||
this.setState({ selectedPosition: { idx: -1, rowIdx: -1 } });
|
||||
};
|
||||
|
||||
selectCell = (cell, openEditor) => {
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_CELL, cell, openEditor);
|
||||
};
|
||||
|
||||
selectStart = (cellPosition) => {
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_START, cellPosition);
|
||||
};
|
||||
|
||||
selectUpdate = (cellPosition, isFromKeyboard, callback) => {
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_UPDATE, cellPosition, isFromKeyboard, callback);
|
||||
};
|
||||
|
||||
selectEnd = () => {
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_END);
|
||||
};
|
||||
|
||||
onCellClick = (cell, e) => {
|
||||
const { selectedPosition } = this.state;
|
||||
if (isShiftKeyDown(e)) {
|
||||
if (!selectedPosition || selectedPosition.idx === -1) {
|
||||
// need select cell first
|
||||
this.selectCell(cell, false);
|
||||
return;
|
||||
}
|
||||
const isFromKeyboard = true;
|
||||
this.selectUpdate(cell, isFromKeyboard);
|
||||
} else {
|
||||
const { columns, recordGetterByIndex, checkCanModifyRecord } = this.props;
|
||||
const column = getColumnByIndex(cell.idx, columns);
|
||||
const supportOpenEditor = checkIsColumnSupportDirectEdit(column);
|
||||
const hasOpenPermission = checkIsCellSupportOpenEditor(cell, column, false, recordGetterByIndex, checkCanModifyRecord);
|
||||
this.selectCell(cell, supportOpenEditor && hasOpenPermission);
|
||||
}
|
||||
this.props.onCellClick(cell);
|
||||
this.setState({ selectedPosition: cell });
|
||||
};
|
||||
|
||||
onCellDoubleClick = (cell, e) => {
|
||||
const { columns, recordGetterByIndex, checkCanModifyRecord } = this.props;
|
||||
const column = getColumnByIndex(cell.idx, columns);
|
||||
const supportOpenEditor = checkEditableViaClickCell(column);
|
||||
const hasOpenPermission = checkIsCellSupportOpenEditor(cell, column, false, recordGetterByIndex, checkCanModifyRecord);
|
||||
this.selectCell(cell, supportOpenEditor && hasOpenPermission);
|
||||
};
|
||||
|
||||
// onRangeSelectStart
|
||||
onCellMouseDown = (cellPosition, event) => {
|
||||
if (!isShiftKeyDown(event)) {
|
||||
this.selectCell(cellPosition);
|
||||
this.selectStart(cellPosition);
|
||||
window.addEventListener('mouseup', this.onWindowMouseUp);
|
||||
}
|
||||
};
|
||||
|
||||
// onRangeSelectUpdate
|
||||
onCellMouseEnter = (cellPosition) => {
|
||||
this.selectUpdate(cellPosition, false, this.updateViewableArea);
|
||||
};
|
||||
|
||||
onCellMouseMove = (cellPosition) => {
|
||||
this.selectUpdate(cellPosition, false, this.updateViewableArea);
|
||||
};
|
||||
|
||||
onWindowMouseUp = (event) => {
|
||||
window.removeEventListener('mouseup', this.onWindowMouseUp);
|
||||
if (isShiftKeyDown(event)) return;
|
||||
this.selectEnd();
|
||||
this.clearHorizontalScroll();
|
||||
};
|
||||
|
||||
onCellRangeSelectionUpdated = (selectedRange) => {
|
||||
this.props.onCellRangeSelectionUpdated(selectedRange);
|
||||
};
|
||||
|
||||
onCellContextMenu = (cellPosition) => {
|
||||
this.setState({
|
||||
selectedPosition: Object.assign({}, this.state.selectedPosition, cellPosition),
|
||||
});
|
||||
this.props.onCellContextMenu(cellPosition);
|
||||
};
|
||||
|
||||
/**
|
||||
* When updating the selection by moving the mouse, you need to automatically scroll to expand the visible area
|
||||
* @param {object} selectedRange
|
||||
*/
|
||||
updateViewableArea = (selectedRange) => {
|
||||
const { sequenceColumnWidth } = this.props;
|
||||
const { mousePosition } = selectedRange.cursorCell;
|
||||
const { x: mouseX, y: mouseY } = mousePosition;
|
||||
const tableHeaderHeight = 50 + 48 + 32;
|
||||
const interval = 100;
|
||||
const step = 8;
|
||||
|
||||
// cursor is at right boundary
|
||||
if (mouseX + interval > window.innerWidth) {
|
||||
this.scrollToRight();
|
||||
} else if (mouseX - interval < sequenceColumnWidth + this.props.frozenColumnsWidth) {
|
||||
// cursor is at left boundary
|
||||
this.scrollToLeft();
|
||||
} else if (mouseY + interval > window.innerHeight - tableHeaderHeight) {
|
||||
// cursor is at bottom boundary
|
||||
const scrollTop = this.getScrollTop();
|
||||
this.resultContentRef.scrollTop = scrollTop + step;
|
||||
this.clearHorizontalScroll();
|
||||
} else if (mouseY - interval < tableHeaderHeight) {
|
||||
// cursor is at top boundary
|
||||
const scrollTop = this.getScrollTop();
|
||||
if (scrollTop - 16 >= 0) {
|
||||
this.resultContentRef.scrollTop = scrollTop - step;
|
||||
}
|
||||
this.clearHorizontalScroll();
|
||||
} else {
|
||||
// cursor is at middle area
|
||||
this.clearHorizontalScroll();
|
||||
}
|
||||
};
|
||||
|
||||
scrollToRight = () => {
|
||||
if (this.timer) return;
|
||||
this.timer = setInterval(() => {
|
||||
const scrollLeft = this.props.getScrollLeft();
|
||||
this.props.setRecordsScrollLeft(scrollLeft + 20);
|
||||
}, 10);
|
||||
};
|
||||
|
||||
scrollToLeft = () => {
|
||||
if (this.timer) return;
|
||||
this.timer = setInterval(() => {
|
||||
const scrollLeft = this.props.getScrollLeft();
|
||||
if (scrollLeft <= 0) {
|
||||
this.clearHorizontalScroll();
|
||||
return;
|
||||
}
|
||||
this.props.setRecordsScrollLeft(scrollLeft - 20);
|
||||
}, 10);
|
||||
};
|
||||
|
||||
clearHorizontalScroll = () => {
|
||||
if (!this.timer) return;
|
||||
clearInterval(this.timer);
|
||||
this.timer = null;
|
||||
};
|
||||
|
||||
clearScrollbarTimer = () => {
|
||||
if (!this.scrollbarTimer) return;
|
||||
clearTimeout(this.scrollbarTimer);
|
||||
this.scrollbarTimer = null;
|
||||
};
|
||||
|
||||
getCellMetaData = () => {
|
||||
if (!this.cellMetaData) {
|
||||
this.cellMetaData = {
|
||||
CellOperationBtn: this.props.CellOperationBtn,
|
||||
onCellClick: this.onCellClick,
|
||||
onCellDoubleClick: this.onCellDoubleClick,
|
||||
onCellMouseDown: this.onCellMouseDown,
|
||||
onCellMouseEnter: this.onCellMouseEnter,
|
||||
onCellMouseMove: this.onCellMouseMove,
|
||||
onDragEnter: this.handleDragEnter,
|
||||
modifyRecord: this.props.modifyRecord,
|
||||
onCellContextMenu: this.onCellContextMenu,
|
||||
};
|
||||
}
|
||||
return this.cellMetaData;
|
||||
};
|
||||
|
||||
handleDragEnter = ({ overRecordIdx, overGroupRecordIndex }) => {
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.DRAG_ENTER, { overRecordIdx, overGroupRecordIndex });
|
||||
};
|
||||
|
||||
setRightScrollbar = (ref) => {
|
||||
this.rightScrollbar = ref;
|
||||
};
|
||||
|
||||
setInteractionMaskRef = (ref) => {
|
||||
this.interactionMask = ref;
|
||||
};
|
||||
|
||||
setResultRef = (ref) => {
|
||||
this.resultRef = ref;
|
||||
};
|
||||
|
||||
getRecordsWrapperScrollHeight = () => {
|
||||
return (this.resultRef && this.resultRef.scrollHeight) || 0;
|
||||
};
|
||||
|
||||
setResultContentRef = (ref) => {
|
||||
this.resultContentRef = ref;
|
||||
};
|
||||
|
||||
getCanvasClientHeight = () => {
|
||||
return (this.resultContentRef && this.resultContentRef.clientHeight) || 0;
|
||||
};
|
||||
|
||||
renderRecords = () => {
|
||||
this.recordFrozenRefs = [];
|
||||
const {
|
||||
recordsCount, columns, sequenceColumnWidth, colOverScanStartIdx, colOverScanEndIdx, lastFrozenColumnKey,
|
||||
recordMetrics, showSequenceColumn, showCellColoring, columnColors,
|
||||
} = this.props;
|
||||
const { startRenderIndex, endRenderIndex, selectedPosition } = this.state;
|
||||
const cellMetaData = this.getCellMetaData();
|
||||
const lastRecordIndex = recordsCount - 1;
|
||||
const shownRecordIds = this.getShownRecordIds();
|
||||
const scrollLeft = this.props.getScrollLeft();
|
||||
const rowHeight = this.getRowHeight();
|
||||
let shownRecords = shownRecordIds.map((recordId, index) => {
|
||||
const record = this.props.recordGetterById(recordId);
|
||||
const isSelected = RecordMetrics.isRecordSelected(recordId, recordMetrics);
|
||||
const recordIndex = startRenderIndex + index;
|
||||
const isLastRecord = lastRecordIndex === recordIndex;
|
||||
const hasSelectedCell = this.props.hasSelectedCell({ recordIndex }, selectedPosition);
|
||||
const columnColor = showCellColoring ? columnColors[recordId] : {};
|
||||
return (
|
||||
<Record
|
||||
key={recordId || recordIndex}
|
||||
ref={ref => {
|
||||
this.recordFrozenRefs.push(ref);
|
||||
}}
|
||||
isSelected={isSelected}
|
||||
index={recordIndex}
|
||||
isLastRecord={isLastRecord}
|
||||
showSequenceColumn={showSequenceColumn}
|
||||
record={record}
|
||||
columns={columns}
|
||||
sequenceColumnWidth={sequenceColumnWidth}
|
||||
colOverScanStartIdx={colOverScanStartIdx}
|
||||
colOverScanEndIdx={colOverScanEndIdx}
|
||||
lastFrozenColumnKey={lastFrozenColumnKey}
|
||||
scrollLeft={scrollLeft}
|
||||
height={rowHeight}
|
||||
cellMetaData={cellMetaData}
|
||||
columnColor={columnColor}
|
||||
searchResult={this.props.searchResult}
|
||||
checkCanModifyRecord={this.props.checkCanModifyRecord}
|
||||
checkCellValueChanged={this.props.checkCellValueChanged}
|
||||
hasSelectedCell={hasSelectedCell}
|
||||
selectedPosition={this.state.selectedPosition}
|
||||
selectNoneCells={this.selectNoneCells}
|
||||
onSelectRecord={this.props.onSelectRecord}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const upperHeight = startRenderIndex * ROW_HEIGHT;
|
||||
const belowHeight = (recordsCount - endRenderIndex) * ROW_HEIGHT;
|
||||
|
||||
// add top placeholder
|
||||
if (upperHeight > 0) {
|
||||
const style = { height: upperHeight, width: '100%' };
|
||||
const upperRow = <div key="upper-placeholder" className="d-flex align-items-end" style={style}><Loading /></div>;
|
||||
shownRecords.unshift(upperRow);
|
||||
}
|
||||
|
||||
// add bottom placeholder
|
||||
if (belowHeight > 0) {
|
||||
const style = { height: belowHeight, width: '100%' };
|
||||
const belowRow = <div key="below-placeholder" style={style}><Loading /></div>;
|
||||
shownRecords.push(belowRow);
|
||||
}
|
||||
return shownRecords;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Fragment>
|
||||
<div
|
||||
id="canvas"
|
||||
className="sf-table-canvas"
|
||||
ref={this.setResultContentRef}
|
||||
onScroll={this.onScroll}
|
||||
onKeyDown={this.props.onGridKeyDown}
|
||||
onKeyUp={this.props.onGridKeyUp}
|
||||
>
|
||||
<InteractionMasks
|
||||
{...this.props}
|
||||
ref={this.setInteractionMaskRef}
|
||||
contextMenu={this.props.contextMenu}
|
||||
canAddRow={this.props.canAddRow}
|
||||
tableId={this.props.tableId}
|
||||
columns={this.props.columns}
|
||||
recordsCount={this.props.recordsCount}
|
||||
recordMetrics={this.props.recordMetrics}
|
||||
rowHeight={this.getRowHeight()}
|
||||
getRowTop={this.getRowTop}
|
||||
scrollTop={this.oldScrollTop}
|
||||
getScrollLeft={this.props.getScrollLeft}
|
||||
getTableContentRect={this.props.getTableContentRect}
|
||||
getMobileFloatIconStyle={this.props.getMobileFloatIconStyle}
|
||||
onToggleMobileMoreOperations={this.props.onToggleMobileMoreOperations}
|
||||
editorPortalTarget={this.props.editorPortalTarget}
|
||||
onCellRangeSelectionUpdated={this.onCellRangeSelectionUpdated}
|
||||
recordGetterByIndex={this.props.recordGetterByIndex}
|
||||
recordGetterById={this.props.recordGetterById}
|
||||
editMobileCell={this.props.editMobileCell}
|
||||
frozenColumnsWidth={this.props.frozenColumnsWidth}
|
||||
selectNone={this.selectNone}
|
||||
getVisibleIndex={this.getVisibleIndex}
|
||||
onHitBottomBoundary={this.onHitBottomCanvas}
|
||||
onHitTopBoundary={this.onHitTopCanvas}
|
||||
onCellClick={this.onCellClick}
|
||||
scrollToColumn={this.scrollToColumn}
|
||||
setRecordsScrollLeft={this.props.setRecordsScrollLeft}
|
||||
getUpdateDraggedRecords={this.props.getUpdateDraggedRecords}
|
||||
getCopiedRecordsAndColumnsFromRange={this.props.getCopiedRecordsAndColumnsFromRange}
|
||||
getTableCanvasContainerRect={this.props.getTableCanvasContainerRect}
|
||||
/>
|
||||
<div className="sf-table-records-wrapper" style={{ width: this.props.totalWidth + this.props.sequenceColumnWidth }} ref={this.setResultRef}>
|
||||
{this.renderRecords()}
|
||||
</div>
|
||||
</div>
|
||||
<RightScrollbar
|
||||
ref={this.setRightScrollbar}
|
||||
getClientHeight={this.getCanvasClientHeight}
|
||||
getScrollHeight={this.getRecordsWrapperScrollHeight}
|
||||
onScrollbarScroll={this.onScrollbarScroll}
|
||||
onScrollbarMouseUp={this.onScrollbarMouseUp}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RecordsBody.propTypes = {
|
||||
onRef: PropTypes.func,
|
||||
contextMenu: PropTypes.oneOfType([PropTypes.node, PropTypes.element]),
|
||||
canAddRow: PropTypes.bool,
|
||||
tableId: PropTypes.string,
|
||||
recordIds: PropTypes.array,
|
||||
recordsCount: PropTypes.number,
|
||||
columns: PropTypes.array.isRequired,
|
||||
CellOperationBtn: PropTypes.object,
|
||||
colOverScanStartIdx: PropTypes.number,
|
||||
colOverScanEndIdx: PropTypes.number,
|
||||
lastFrozenColumnKey: PropTypes.string,
|
||||
showSequenceColumn: PropTypes.bool,
|
||||
sequenceColumnWidth: PropTypes.number,
|
||||
hasSelectedRecord: PropTypes.bool,
|
||||
recordMetrics: PropTypes.object,
|
||||
totalWidth: PropTypes.number,
|
||||
getScrollLeft: PropTypes.func,
|
||||
setRecordsScrollLeft: PropTypes.func,
|
||||
storeScrollPosition: PropTypes.func,
|
||||
hasSelectedCell: PropTypes.func,
|
||||
scrollToLoadMore: PropTypes.func,
|
||||
getTableContentRect: PropTypes.func,
|
||||
getMobileFloatIconStyle: PropTypes.func,
|
||||
onToggleMobileMoreOperations: PropTypes.func,
|
||||
onToggleInsertRecordDialog: PropTypes.func,
|
||||
editorPortalTarget: PropTypes.instanceOf(Element),
|
||||
recordGetterByIndex: PropTypes.func,
|
||||
recordGetterById: PropTypes.func,
|
||||
modifyRecord: PropTypes.func,
|
||||
selectNone: PropTypes.func,
|
||||
onCellClick: PropTypes.func,
|
||||
onCellRangeSelectionUpdated: PropTypes.func,
|
||||
onSelectRecord: PropTypes.func,
|
||||
checkCanModifyRecord: PropTypes.func,
|
||||
deleteRecordsLinks: PropTypes.func,
|
||||
paste: PropTypes.func,
|
||||
searchResult: PropTypes.object,
|
||||
scrollToRowIndex: PropTypes.number,
|
||||
frozenColumnsWidth: PropTypes.number,
|
||||
editMobileCell: PropTypes.func,
|
||||
reloadRecords: PropTypes.func,
|
||||
appPage: PropTypes.object,
|
||||
showCellColoring: PropTypes.bool,
|
||||
columnColors: PropTypes.object,
|
||||
onFillingDragRows: PropTypes.func,
|
||||
getUpdateDraggedRecords: PropTypes.func,
|
||||
getCopiedRecordsAndColumnsFromRange: PropTypes.func,
|
||||
openDownloadFilesDialog: PropTypes.func,
|
||||
cacheDownloadFilesProps: PropTypes.func,
|
||||
onCellContextMenu: PropTypes.func,
|
||||
getTableCanvasContainerRect: PropTypes.func,
|
||||
};
|
||||
|
||||
export default RecordsBody;
|
@@ -0,0 +1,75 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import GroupHeaderLeft from './group-header-left';
|
||||
import { GROUP_FROZEN_HEADER as Z_INDEX_GROUP_FROZEN_HEADER } from '../../../../constants/z-index';
|
||||
|
||||
class GroupContainerLeft extends Component {
|
||||
|
||||
fixedFrozenDOMs = (scrollLeft, scrollTop) => {
|
||||
if (this.leftContainer) {
|
||||
this.leftContainer.style.position = 'fixed';
|
||||
this.leftContainer.style.marginLeft = '0px';
|
||||
this.leftContainer.style.marginTop = (-scrollTop) + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
setContainerRef = (ref) => {
|
||||
this.leftContainer = ref;
|
||||
};
|
||||
|
||||
cancelFixFrozenDOMs = (scrollLeft) => {
|
||||
if (this.leftContainer) {
|
||||
this.leftContainer.style.position = 'absolute';
|
||||
this.leftContainer.style.marginLeft = scrollLeft + 'px';
|
||||
this.leftContainer.style.marginTop = '0px';
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
isExpanded, maxLevel, group, formulaRow, leftPaneWidth, height,
|
||||
firstColumnFrozen, lastColumnFrozen, firstColumnKey,
|
||||
} = this.props;
|
||||
let containerStyle = {
|
||||
zIndex: firstColumnFrozen ? Z_INDEX_GROUP_FROZEN_HEADER : 0,
|
||||
width: leftPaneWidth,
|
||||
height,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group-container group-container-left"
|
||||
style={containerStyle}
|
||||
ref={this.setContainerRef}
|
||||
>
|
||||
<GroupHeaderLeft
|
||||
ref={ref => this.leftHeader = ref}
|
||||
isExpanded={isExpanded}
|
||||
firstColumnFrozen={firstColumnFrozen}
|
||||
lastColumnFrozen={lastColumnFrozen}
|
||||
firstColumnKey={firstColumnKey}
|
||||
width={leftPaneWidth}
|
||||
maxLevel={maxLevel}
|
||||
group={group}
|
||||
formulaRow={formulaRow}
|
||||
onExpandGroupToggle={this.props.onExpandGroupToggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GroupContainerLeft.propTypes = {
|
||||
isExpanded: PropTypes.bool,
|
||||
firstColumnFrozen: PropTypes.bool,
|
||||
lastColumnFrozen: PropTypes.bool,
|
||||
firstColumnKey: PropTypes.string,
|
||||
maxLevel: PropTypes.number,
|
||||
group: PropTypes.object,
|
||||
formulaRow: PropTypes.object,
|
||||
leftPaneWidth: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
onExpandGroupToggle: PropTypes.func,
|
||||
};
|
||||
|
||||
export default GroupContainerLeft;
|
@@ -0,0 +1,56 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import GroupHeaderRight from './group-header-right';
|
||||
|
||||
class GroupContainerRight extends Component {
|
||||
|
||||
fixedFrozenDOMs = (scrollLeft, scrollTop) => {
|
||||
this.rightHeader && this.rightHeader.fixedFrozenDOMs(scrollLeft, scrollTop);
|
||||
};
|
||||
|
||||
cancelFixFrozenDOMs = (scrollLeft) => {
|
||||
this.rightHeader && this.rightHeader.cancelFixFrozenDOMs(scrollLeft);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
group, isExpanded, sequenceColumnWidth, columns, summaryConfigs, rightPaneWidth, leftPaneWidth, height,
|
||||
groupOffsetLeft, lastFrozenColumnKey,
|
||||
} = this.props;
|
||||
const groupContainerRightStyle = {
|
||||
left: leftPaneWidth,
|
||||
width: rightPaneWidth,
|
||||
height,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="group-container group-container-right" style={groupContainerRightStyle}>
|
||||
<GroupHeaderRight
|
||||
ref={ref => this.rightHeader = ref}
|
||||
groupOffsetLeft={groupOffsetLeft}
|
||||
lastFrozenColumnKey={lastFrozenColumnKey}
|
||||
group={group}
|
||||
isExpanded={isExpanded}
|
||||
columns={columns}
|
||||
sequenceColumnWidth={sequenceColumnWidth}
|
||||
summaryConfigs={summaryConfigs}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GroupContainerRight.propTypes = {
|
||||
group: PropTypes.object,
|
||||
isExpanded: PropTypes.bool,
|
||||
sequenceColumnWidth: PropTypes.number,
|
||||
columns: PropTypes.array,
|
||||
summaryConfigs: PropTypes.object,
|
||||
rightPaneWidth: PropTypes.number,
|
||||
leftPaneWidth: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
groupOffsetLeft: PropTypes.number,
|
||||
lastFrozenColumnKey: PropTypes.string,
|
||||
};
|
||||
|
||||
export default GroupContainerRight;
|
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { checkIsColumnFrozen } from '../../../../utils/column';
|
||||
import { GROUP_HEADER_HEIGHT } from '../../../../constants/group';
|
||||
import { GROUP_FROZEN_HEADER as Z_INDEX_GROUP_FROZEN_HEADER } from '../../../../constants/z-index';
|
||||
|
||||
class GroupHeaderCell extends React.PureComponent {
|
||||
|
||||
fixedFrozenDOMs = (scrollLeft, scrollTop) => {
|
||||
if (this.headerCell) {
|
||||
const { firstColumnWidth, groupOffsetLeft, sequenceColumnWidth } = this.props;
|
||||
this.headerCell.style.position = 'fixed';
|
||||
this.headerCell.style.marginLeft = (sequenceColumnWidth + firstColumnWidth + groupOffsetLeft) + 'px';
|
||||
this.headerCell.style.marginTop = (-scrollTop) + 'px';
|
||||
}
|
||||
};
|
||||
|
||||
cancelFixFrozenDOMs = (scrollLeft) => {
|
||||
if (this.headerCell) {
|
||||
this.headerCell.style.position = 'absolute';
|
||||
this.headerCell.style.marginLeft = scrollLeft + 'px';
|
||||
this.headerCell.style.marginTop = 0;
|
||||
}
|
||||
};
|
||||
|
||||
getStyle = () => {
|
||||
let { offsetLeft, column, isExpanded } = this.props;
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
width: column.width,
|
||||
height: GROUP_HEADER_HEIGHT - (isExpanded ? 1 : 2), // header height - border-top(1px) - !isExpanded && border-bottom(1px)
|
||||
left: offsetLeft
|
||||
};
|
||||
if (checkIsColumnFrozen(column)) {
|
||||
style.zIndex = Z_INDEX_GROUP_FROZEN_HEADER;
|
||||
}
|
||||
return style;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { column, isLastFrozenColumn } = this.props;
|
||||
return (
|
||||
<div
|
||||
ref={ref => this.headerCell = ref}
|
||||
className={classnames('summary-item group-header-cell', {
|
||||
'table-last--frozen': isLastFrozenColumn
|
||||
})}
|
||||
style={this.getStyle()}
|
||||
data-column_key={column.key}
|
||||
>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GroupHeaderCell.propTypes = {
|
||||
column: PropTypes.object.isRequired,
|
||||
isExpanded: PropTypes.bool,
|
||||
isLastFrozenColumn: PropTypes.bool,
|
||||
firstColumnWidth: PropTypes.number,
|
||||
offsetLeft: PropTypes.number.isRequired,
|
||||
groupOffsetLeft: PropTypes.number,
|
||||
summary: PropTypes.object,
|
||||
summaryMethod: PropTypes.string,
|
||||
};
|
||||
|
||||
export default GroupHeaderCell;
|
@@ -0,0 +1,66 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { IconBtn } from '@seafile/sf-metadata-ui-component';
|
||||
import GroupTitle from './group-title';
|
||||
import { GROUP_HEADER_HEIGHT } from '../../../../constants/group';
|
||||
import { GROUP_FROZEN_HEADER as Z_INDEX_GROUP_FROZEN_HEADER } from '../../../../constants/z-index';
|
||||
import { gettext } from '../../../../../../utils/constants';
|
||||
|
||||
class GroupHeaderLeft extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
isExpanded, firstColumnFrozen, lastColumnFrozen, firstColumnKey, maxLevel,
|
||||
group, width,
|
||||
} = this.props;
|
||||
const { column, count, level, cell_value, original_cell_value } = group;
|
||||
let groupHeaderLeftStyle = {
|
||||
height: GROUP_HEADER_HEIGHT,
|
||||
width,
|
||||
};
|
||||
if (firstColumnFrozen) {
|
||||
groupHeaderLeftStyle.zIndex = Z_INDEX_GROUP_FROZEN_HEADER;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref => this.groupHeaderLeft = ref}
|
||||
className={classnames('group-header-left group-header-cell', { 'table-last--frozen': lastColumnFrozen })}
|
||||
style={groupHeaderLeftStyle}
|
||||
data-column_key={firstColumnKey}
|
||||
>
|
||||
<IconBtn
|
||||
className={classnames('group-toggle-btn', { 'hide': !isExpanded })}
|
||||
iconName="drop-down"
|
||||
onClick={this.props.onExpandGroupToggle}
|
||||
/>
|
||||
<GroupTitle
|
||||
column={column || {}}
|
||||
originalCellValue={original_cell_value}
|
||||
cellValue={cell_value}
|
||||
/>
|
||||
<div className="group-rows-count">
|
||||
<div className="group-rows-count-content">
|
||||
{level === maxLevel && <span className="count-title">{gettext('Count')}</span>}
|
||||
<span className="count-num">{count}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GroupHeaderLeft.propTypes = {
|
||||
isExpanded: PropTypes.bool,
|
||||
firstColumnFrozen: PropTypes.bool,
|
||||
lastColumnFrozen: PropTypes.bool,
|
||||
firstColumnKey: PropTypes.string,
|
||||
maxLevel: PropTypes.number,
|
||||
group: PropTypes.object,
|
||||
formulaRow: PropTypes.object,
|
||||
width: PropTypes.number,
|
||||
onExpandGroupToggle: PropTypes.func,
|
||||
};
|
||||
|
||||
export default GroupHeaderLeft;
|
@@ -0,0 +1,85 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import GroupHeaderCell from './group-header-cell';
|
||||
import { GROUP_HEADER_HEIGHT } from '../../../../constants/group';
|
||||
import { checkIsColumnFrozen } from '../../../../utils/column';
|
||||
|
||||
class GroupHeaderRight extends Component {
|
||||
|
||||
headerCells = {};
|
||||
|
||||
setHeaderCellRef = (key) => (node) => {
|
||||
this.headerCells[key] = node;
|
||||
};
|
||||
|
||||
fixedFrozenDOMs = (scrollLeft, scrollTop) => {
|
||||
this.props.columns.forEach((column) => {
|
||||
const headerCell = this.headerCells[column.key];
|
||||
if (checkIsColumnFrozen(column) && headerCell) {
|
||||
headerCell.fixedFrozenDOMs(scrollLeft, scrollTop);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
cancelFixFrozenDOMs = (scrollLeft) => {
|
||||
this.props.columns.forEach((column) => {
|
||||
const headerCell = this.headerCells[column.key];
|
||||
if (checkIsColumnFrozen(column) && headerCell) {
|
||||
headerCell.cancelFixFrozenDOMs(scrollLeft);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
getGroupSummaries = () => {
|
||||
const {
|
||||
group, isExpanded, sequenceColumnWidth, columns, groupOffsetLeft, lastFrozenColumnKey, summaryConfigs,
|
||||
} = this.props;
|
||||
const summaryColumns = columns.slice(1); // get column from 2 index
|
||||
const firstColumnWidth = columns[0] ? columns[0].width : 0;
|
||||
let offsetLeft = 0;
|
||||
return summaryColumns.map((column, index) => {
|
||||
const { key } = column;
|
||||
const summaryMethod = summaryConfigs && summaryConfigs[key] ? summaryConfigs[key] : 'Sum';
|
||||
const summary = group.summaries[key];
|
||||
if (index !== 0) {
|
||||
offsetLeft += summaryColumns[index - 1].width;
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupHeaderCell
|
||||
key={key}
|
||||
ref={this.setHeaderCellRef(key)}
|
||||
firstColumnWidth={firstColumnWidth}
|
||||
groupOffsetLeft={groupOffsetLeft}
|
||||
isLastFrozenColumn={key === lastFrozenColumnKey}
|
||||
offsetLeft={offsetLeft}
|
||||
column={column}
|
||||
sequenceColumnWidth={sequenceColumnWidth}
|
||||
isExpanded={isExpanded}
|
||||
summary={summary}
|
||||
summaryMethod={summaryMethod}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="group-header-right" style={{ height: GROUP_HEADER_HEIGHT }}>
|
||||
{this.getGroupSummaries()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GroupHeaderRight.propTypes = {
|
||||
group: PropTypes.object,
|
||||
isExpanded: PropTypes.bool,
|
||||
groupOffsetLeft: PropTypes.number,
|
||||
lastFrozenColumnKey: PropTypes.string,
|
||||
columns: PropTypes.array,
|
||||
sequenceColumnWidth: PropTypes.number,
|
||||
summaryConfigs: PropTypes.object,
|
||||
};
|
||||
|
||||
export default GroupHeaderRight;
|
@@ -0,0 +1,27 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { gettext } from '../../../../../../utils/constants';
|
||||
|
||||
const EMPTY_TIP = `(${gettext('Empty')})`;
|
||||
|
||||
const GroupTitle = ({ column, cellValue }) => {
|
||||
|
||||
const renderGroupCellVal = useCallback(() => {
|
||||
return cellValue || EMPTY_TIP;
|
||||
}, [cellValue]);
|
||||
|
||||
return (
|
||||
<div className="group-title">
|
||||
<div className="group-column-name">{column.name}</div>
|
||||
<div className="group-cell-value">{renderGroupCellVal()}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
GroupTitle.propTypes = {
|
||||
originalCellValue: PropTypes.any,
|
||||
cellValue: PropTypes.any,
|
||||
column: PropTypes.object,
|
||||
};
|
||||
|
||||
export default GroupTitle;
|
@@ -0,0 +1,395 @@
|
||||
.canvas-groups-rows {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .sf-table-cell {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-item {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-container-left,
|
||||
.canvas-groups-rows .group-container-right {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* border-radius of group container */
|
||||
.canvas-groups-rows .group-item .group-container-left {
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-item .group-container-right {
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
/* background of group container */
|
||||
.canvas-groups-rows .group-level-1 .group-container-left,
|
||||
.canvas-groups-rows .group-level-1 .group-container-right {
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-level-2 .group-container-left,
|
||||
.canvas-groups-rows .group-level-2 .group-container-right {
|
||||
background-color: #ededed;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-level-3 .group-container-left,
|
||||
.canvas-groups-rows .group-level-3 .group-container-right {
|
||||
background-color: #e3e3e3;
|
||||
}
|
||||
|
||||
/* border-color of group container */
|
||||
.canvas-groups-rows .group-level-2 .group-container-left {
|
||||
border-left: 1px solid #c1c1c1;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-level-3 .group-container-left {
|
||||
border-left: 1px solid #c1c1c1;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-level-2 .group-container-right {
|
||||
border-right: 1px solid #c1c1c1;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-level-3 .group-container-right {
|
||||
border-right: 1px solid #c1c1c1;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .expanded-group.group-level-2 .group-container-left,
|
||||
.canvas-groups-rows .expanded-group.group-level-2 .group-container-right {
|
||||
border-bottom: 1px solid #c1c1c1;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .expanded-group.group-level-3 .group-container-left,
|
||||
.canvas-groups-rows .expanded-group.group-level-3 .group-container-right {
|
||||
border-bottom: 1px solid #c1c1c1;
|
||||
}
|
||||
|
||||
/* border-color of group header */
|
||||
.canvas-groups-rows .group-level-1 .group-header-left {
|
||||
border-left: 1px solid #cacaca;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-level-1 .group-header-right {
|
||||
border-right: 1px solid #cacaca;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-level-1 .group-header-left,
|
||||
.canvas-groups-rows .group-level-1 .group-header-right {
|
||||
border-top: 1px solid #cacaca;
|
||||
border-bottom: 1px solid #cacaca;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-level-2 .group-header-left,
|
||||
.canvas-groups-rows .group-level-2 .group-header-right {
|
||||
border-top: 1px solid #c1c1c1;
|
||||
border-bottom: 1px solid #c1c1c1;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-level-3 .group-header-left,
|
||||
.canvas-groups-rows .group-level-3 .group-header-right {
|
||||
border-top: 1px solid #c1c1c1;
|
||||
border-bottom: 1px solid #c1c1c1;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .expanded-group .group-header-left,
|
||||
.canvas-groups-rows .expanded-group .group-header-right {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* group backdrop */
|
||||
.canvas-groups-rows .group-item .group-backdrop {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
background-color: #fff;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* group-header-left */
|
||||
.canvas-groups-rows .group-header-left {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top-left-radius: 5px;
|
||||
background-color: inherit;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .folded-group .group-header-left {
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
.canvas-groups-rows.single-column .group-level-1 .group-container-left,
|
||||
.canvas-groups-rows.single-column .group-level-1 .group-header-cell {
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.canvas-groups-rows.single-column .group-level-1 .group-header-cell {
|
||||
border-right: 1px solid #cacaca !important;
|
||||
}
|
||||
|
||||
.canvas-groups-rows.single-column .folded-group .group-container-left,
|
||||
.canvas-groups-rows.single-column .folded-group .group-header-cell,
|
||||
.canvas-groups-rows.single-column.frozen .table-btn-add-record {
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
/* group header cell */
|
||||
.canvas-groups-rows .group-level-1 .group-header-cell {
|
||||
border-right: 1px solid #ededed;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-level-2 .group-header-cell {
|
||||
border-right: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-level-3 .group-header-cell {
|
||||
border-right: 1px solid #dadada;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-container-right .group-header-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.canvas-groups-rows.all-columns-frozen .group-level-2 .table-last--frozen,
|
||||
.canvas-groups-rows.all-columns-frozen .group-level-3 .table-last--frozen {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
/* group expand */
|
||||
.canvas-groups-rows .group-toggle-btn {
|
||||
margin: 0 8px;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-toggle-btn .sf-metadata-icon-drop-down {
|
||||
font-size: 12px;
|
||||
fill: #666666;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-toggle-btn.hide .sf-metadata-icon-drop-down {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* group title */
|
||||
.canvas-groups-rows .group-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-title .group-column-name {
|
||||
font-size: 12px;
|
||||
color: #666666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-title .group-cell-value {
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .sf-metadata-ui.cell-formatter-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* group rows count */
|
||||
.canvas-groups-rows .group-rows-count {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.group-rows-count-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 0 15px 0 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-item.group-level-1 .group-rows-count {
|
||||
background: linear-gradient(to right, hsla(0, 0%, 97%, 0), hsl(0, 0%, 97%) 18%);
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-item.group-level-2 .group-rows-count {
|
||||
background: linear-gradient(to right, hsla(0, 0%, 93%, 0), hsl(0, 0%, 93%) 18%);
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-item.group-level-3 .group-rows-count {
|
||||
background: linear-gradient(to right, hsla(0, 0%, 89%, 0), hsl(0, 0%, 89%) 18%);
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-rows-count .count-title {
|
||||
margin-right: 4px;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
/* group-header-right */
|
||||
.canvas-groups-rows .group-header-right {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
display: inline-flex;
|
||||
overflow: hidden;
|
||||
border-top-right-radius: 5px;
|
||||
background-color: inherit;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .folded-group .group-header-right {
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
.canvas-groups-rows:not(.single-column) .group-level-2 .group-header-right {
|
||||
border-left: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.canvas-groups-rows:not(.single-column) .group-level-3 .group-header-right {
|
||||
border-left: 1px solid #dadada;
|
||||
}
|
||||
|
||||
/* group summary */
|
||||
.canvas-groups-rows .summary-item {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
/* group-row-cell */
|
||||
.canvas-groups-rows .sf-table-row {
|
||||
position: absolute;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sf-table-canvas .canvas-groups-rows .sf-table-row {
|
||||
background-color: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .sf-table-row .sf-table-cell {
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* border color of last cell within group view */
|
||||
.canvas-groups-rows.disabled-add-record .sf-table-row.sf-table-last-row .sf-table-cell {
|
||||
border-bottom: 1px solid #cacaca;
|
||||
}
|
||||
|
||||
.canvas-groups-rows.frozen .table-result-table-cell.actions-cell {
|
||||
z-index: 2 !important;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .actions-cell {
|
||||
border-left: 1px solid #cacaca;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .sf-table-row .last-cell {
|
||||
border-right: 1px solid #cacaca;
|
||||
}
|
||||
|
||||
.canvas-groups-rows.disabled-add-record .sf-table-last-row,
|
||||
.canvas-groups-rows.disabled-add-record .sf-table-last-row .actions-cell {
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
.canvas-groups-rows.disabled-add-record .sf-table-last-row,
|
||||
.canvas-groups-rows.disabled-add-record .sf-table-last-row .last-cell {
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
/* animation */
|
||||
.canvas-groups-rows.animation {
|
||||
transition-property: height;
|
||||
-webkit-transition-property: height;
|
||||
-moz-transition-property: height;
|
||||
transition-duration: 0.3s;
|
||||
-webkit-transition-duration: 0.3s;
|
||||
-moz-transition-duration: 0.3s;
|
||||
transition-timing-function: ease-out;
|
||||
-webkit-transition-timing-function: ease-out;
|
||||
-moz-transition-timing-function: ease-out;
|
||||
transition-delay: 0s;
|
||||
-webkit-transition-delay: 0s;
|
||||
-moz-transition-delay: 0s;
|
||||
}
|
||||
|
||||
.canvas-groups-rows.animation .group-item,
|
||||
.canvas-groups-rows.animation .sf-table-row {
|
||||
transition-property: top;
|
||||
-webkit-transition-property: top;
|
||||
-moz-transition-property: top;
|
||||
transition-duration: 0.3s;
|
||||
-webkit-transition-duration: 0.3s;
|
||||
-moz-transition-duration: 0.3s;
|
||||
transition-timing-function: ease-out;
|
||||
-webkit-transition-timing-function: ease-out;
|
||||
-moz-transition-timing-function: ease-out;
|
||||
transition-delay: 0s;
|
||||
-webkit-transition-delay: 0s;
|
||||
-moz-transition-delay: 0s;
|
||||
}
|
||||
|
||||
.canvas-groups-rows.animation .group-item,
|
||||
.canvas-groups-rows.animation .sf-table-row {
|
||||
transition-property: height, top;
|
||||
-webkit-transition-property: height, top;
|
||||
-moz-transition-property: height, top;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-item.folding {
|
||||
transition-property: none;
|
||||
-webkit-transition-property: none;
|
||||
-moz-transition-property: none;
|
||||
}
|
||||
|
||||
.canvas-groups-rows.single-column .group-item {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.canvas-groups-rows.animation .group-item .group-backdrop,
|
||||
.canvas-groups-rows.animation .group-item .group-container-left,
|
||||
.canvas-groups-rows.animation .group-item .group-container-right {
|
||||
transition-property: height;
|
||||
-webkit-transition-property: height;
|
||||
-moz-transition-property: height;
|
||||
transition-duration: 0.3s;
|
||||
-webkit-transition-duration: 0.3s;
|
||||
-moz-transition-duration: 0.3s;
|
||||
transition-timing-function: ease-out;
|
||||
-webkit-transition-timing-function: ease-out;
|
||||
-moz-transition-timing-function: ease-out;
|
||||
transition-delay: 0s;
|
||||
-webkit-transition-delay: 0s;
|
||||
-moz-transition-delay: 0s;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-item.folding .group-container-left,
|
||||
.canvas-groups-rows .group-item.folding .group-container-right {
|
||||
transition-property: none;
|
||||
-webkit-transition-property: none;
|
||||
-moz-transition-property: none;
|
||||
}
|
||||
|
||||
.canvas-groups-rows .group-title .group-cell-value .sf-metadata-group-title-rate-item .sf-metadata-icon {
|
||||
font-size: 16px;
|
||||
fill: inherit;
|
||||
}
|
@@ -0,0 +1,167 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import joinClasses from 'classnames';
|
||||
import GroupContainerLeft from './group-container-left';
|
||||
import GroupContainerRight from './group-container-right';
|
||||
import { isMobile } from '../../../../../../utils/utils';
|
||||
import { checkIsColumnFrozen } from '../../../../utils/column';
|
||||
import { GROUP_VIEW_OFFSET } from '../../../../constants/group';
|
||||
import { GROUP_BACKDROP as Z_INDEX_GROUP_BACKDROP } from '../../../../constants/z-index';
|
||||
|
||||
import './index.css';
|
||||
|
||||
|
||||
class GroupContainer extends Component {
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return (
|
||||
nextProps.groupPathString !== this.props.groupPathString ||
|
||||
nextProps.group !== this.props.group ||
|
||||
nextProps.width !== this.props.width ||
|
||||
nextProps.height !== this.props.height ||
|
||||
nextProps.top !== this.props.top ||
|
||||
nextProps.columns !== this.props.columns ||
|
||||
nextProps.sequenceColumnWidth !== this.props.sequenceColumnWidth ||
|
||||
nextProps.rowHeight !== this.props.rowHeight ||
|
||||
nextProps.isExpanded !== this.props.isExpanded ||
|
||||
nextProps.scrollLeft !== this.props.scrollLeft ||
|
||||
nextProps.lastFrozenColumnKey !== this.props.lastFrozenColumnKey ||
|
||||
nextProps.summaryConfigs !== this.props.summaryConfigs
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.lastFrozenColumnKey && !isMobile) {
|
||||
this.checkScroll();
|
||||
}
|
||||
}
|
||||
|
||||
checkScroll() {
|
||||
const { scrollLeft } = this.props;
|
||||
this.cancelFixFrozenDOMs(scrollLeft);
|
||||
}
|
||||
|
||||
fixedFrozenDOMs = (scrollLeft, scrollTop) => {
|
||||
if (this.backDrop) {
|
||||
this.backDrop.style.position = 'fixed';
|
||||
this.backDrop.style.marginLeft = '0px';
|
||||
this.backDrop.style.marginTop = (-scrollTop) + 'px';
|
||||
}
|
||||
|
||||
this.leftContainer && this.leftContainer.fixedFrozenDOMs(scrollLeft, scrollTop);
|
||||
this.rightContainer && this.rightContainer.fixedFrozenDOMs(scrollLeft, scrollTop);
|
||||
};
|
||||
|
||||
cancelFixFrozenDOMs = (scrollLeft) => {
|
||||
if (this.backDrop) {
|
||||
this.backDrop.style.position = 'absolute';
|
||||
this.backDrop.style.marginLeft = scrollLeft - GROUP_VIEW_OFFSET + 'px';
|
||||
this.backDrop.style.marginTop = '0px';
|
||||
}
|
||||
|
||||
this.leftContainer && this.leftContainer.cancelFixFrozenDOMs(scrollLeft);
|
||||
this.rightContainer && this.rightContainer.cancelFixFrozenDOMs(scrollLeft);
|
||||
};
|
||||
|
||||
setContainer = (node) => {
|
||||
this.group = node;
|
||||
};
|
||||
|
||||
setBackDrop = (node) => {
|
||||
this.backDrop = node;
|
||||
};
|
||||
|
||||
onExpandGroupToggle = () => {
|
||||
const { groupPathString } = this.props;
|
||||
this.props.onExpandGroupToggle(groupPathString);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
group, columns, width, isExpanded, sequenceColumnWidth, folding, summaryConfigs, height, backdropHeight, top,
|
||||
groupOffsetLeft, lastFrozenColumnKey, maxLevel, scrollLeft,
|
||||
} = this.props;
|
||||
const { left, level } = group;
|
||||
const firstLevelGroup = level === 1;
|
||||
const groupClassName = joinClasses(
|
||||
'group-item',
|
||||
`group-level-${level}`,
|
||||
isExpanded ? 'expanded-group' : 'folded-group',
|
||||
folding ? 'folding' : '',
|
||||
);
|
||||
|
||||
const firstColumn = columns[0] || {};
|
||||
const firstColumnFrozen = checkIsColumnFrozen(firstColumn);
|
||||
const firstColumnWidth = firstColumn.width || 0;
|
||||
const leftPaneWidth = sequenceColumnWidth + firstColumnWidth + (firstLevelGroup ? 0 : ((level - 1) * GROUP_VIEW_OFFSET - 1));
|
||||
const rightPaneWidth = width - leftPaneWidth;
|
||||
const groupItemStyle = {
|
||||
height,
|
||||
width,
|
||||
top,
|
||||
left
|
||||
};
|
||||
let backDropStyle = {
|
||||
height: backdropHeight,
|
||||
width: leftPaneWidth + scrollLeft ? GROUP_VIEW_OFFSET : 0,
|
||||
zIndex: Z_INDEX_GROUP_BACKDROP,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={groupClassName} ref={this.setContainer} style={groupItemStyle}>
|
||||
{(level === maxLevel && firstColumnFrozen) &&
|
||||
<div className="group-backdrop" ref={this.setBackDrop} style={backDropStyle}></div>
|
||||
}
|
||||
<GroupContainerLeft
|
||||
ref={ref => this.leftContainer = ref}
|
||||
group={group}
|
||||
firstColumnFrozen={firstColumnFrozen}
|
||||
lastColumnFrozen={firstColumn.key === lastFrozenColumnKey}
|
||||
leftPaneWidth={leftPaneWidth}
|
||||
height={height}
|
||||
isExpanded={isExpanded}
|
||||
firstColumnKey={firstColumn.key}
|
||||
maxLevel={maxLevel}
|
||||
onExpandGroupToggle={this.onExpandGroupToggle}
|
||||
/>
|
||||
<GroupContainerRight
|
||||
ref={ref => this.rightContainer = ref}
|
||||
group={group}
|
||||
isExpanded={isExpanded}
|
||||
leftPaneWidth={leftPaneWidth}
|
||||
rightPaneWidth={rightPaneWidth}
|
||||
height={height}
|
||||
groupOffsetLeft={groupOffsetLeft}
|
||||
lastFrozenColumnKey={lastFrozenColumnKey}
|
||||
columns={columns}
|
||||
sequenceColumnWidth={sequenceColumnWidth}
|
||||
summaryConfigs={summaryConfigs}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GroupContainer.propTypes = {
|
||||
group: PropTypes.object,
|
||||
groupPathString: PropTypes.string,
|
||||
folding: PropTypes.bool,
|
||||
columns: PropTypes.array,
|
||||
sequenceColumnWidth: PropTypes.number,
|
||||
rowHeight: PropTypes.number,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
backdropHeight: PropTypes.number,
|
||||
top: PropTypes.number,
|
||||
groupOffsetLeft: PropTypes.number,
|
||||
formulaRow: PropTypes.object,
|
||||
lastFrozenColumnKey: PropTypes.string,
|
||||
isExpanded: PropTypes.bool,
|
||||
scrollLeft: PropTypes.number,
|
||||
maxLevel: PropTypes.number,
|
||||
summaryConfigs: PropTypes.object,
|
||||
onExpandGroupToggle: PropTypes.func,
|
||||
updateSummaryConfig: PropTypes.func,
|
||||
};
|
||||
|
||||
export default GroupContainer;
|
File diff suppressed because it is too large
Load Diff
807
frontend/src/components/sf-table/table-main/records/index.js
Normal file
807
frontend/src/components/sf-table/table-main/records/index.js
Normal file
@@ -0,0 +1,807 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { HorizontalScrollbar } from '../../scrollbar';
|
||||
import RecordsHeader from '../records-header';
|
||||
import Body from './body';
|
||||
import GroupBody from './group-body';
|
||||
import RecordsFooter from '../records-footer';
|
||||
import ContextMenu from '../../context-menu';
|
||||
import RecordMetrics from '../../utils/record-metrics';
|
||||
import { recalculate } from '../../utils/column';
|
||||
import { getVisibleBoundaries } from '../../utils/viewport';
|
||||
import { getColOverScanEndIdx, getColOverScanStartIdx } from '../../utils/grid';
|
||||
import { isShiftKeyDown } from '../../../../utils/keyboard-utils';
|
||||
import { isMobile } from '../../../../utils/utils';
|
||||
import { isWindowsBrowser, isWebkitBrowser, addClassName, removeClassName, getEventClassName } from '../../utils';
|
||||
import EventBus from '../../../common/event-bus';
|
||||
import { EVENT_BUS_TYPE } from '../../constants/event-bus-type';
|
||||
import { CANVAS_RIGHT_INTERVAL } from '../../constants/grid';
|
||||
import { GROUP_ROW_TYPE } from '../../constants/group';
|
||||
import { isNumber } from '../../../../utils/number';
|
||||
|
||||
class Records extends Component {
|
||||
|
||||
static defaultProps = {
|
||||
gridScroll: {
|
||||
scroll_left: 0,
|
||||
scroll_top: 0,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.scrollTop = 0;
|
||||
this.isScrollByScrollbar = false;
|
||||
const { scroll_left } = this.getNormalizedScroll();
|
||||
this.scrollLeft = scroll_left;
|
||||
this.lastScrollLeft = this.scrollLeft;
|
||||
this.initPosition = { idx: -1, rowIdx: -1, groupRecordIndex: -1 };
|
||||
const columnMetrics = this.createColumnMetrics(props);
|
||||
const { width: tableContentWidth } = props.getTableContentRect();
|
||||
const initHorizontalScrollState = this.getHorizontalScrollState({ gridWidth: tableContentWidth, columnMetrics, scrollLeft: 0 });
|
||||
this.state = {
|
||||
columnMetrics,
|
||||
recordMetrics: this.createRowMetrics(),
|
||||
lastRowIdxUiSelected: { groupRecordIndex: -1, recordIndex: -1 },
|
||||
touchStartPosition: {},
|
||||
selectedRange: {
|
||||
topLeft: this.initPosition,
|
||||
bottomRight: this.initPosition,
|
||||
},
|
||||
selectedPosition: this.initPosition,
|
||||
...initHorizontalScrollState,
|
||||
};
|
||||
this.eventBus = EventBus.getInstance();
|
||||
this.isWindows = isWindowsBrowser();
|
||||
this.isWebkit = isWebkitBrowser();
|
||||
this.deletedRecord = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('copy', this.onCopyCells);
|
||||
document.addEventListener('paste', this.onPasteCells);
|
||||
document.addEventListener('cut', this.onCutCells);
|
||||
if (window.isMobile) {
|
||||
window.addEventListener('touchstart', this.onTouchStart);
|
||||
window.addEventListener('touchend', this.onTouchEnd);
|
||||
} else {
|
||||
document.addEventListener('mousedown', this.onMouseDown);
|
||||
}
|
||||
this.unsubscribeSelectNone = this.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_NONE, this.selectNone);
|
||||
this.unsubscribeSelectCell = this.eventBus.subscribe(EVENT_BUS_TYPE.SELECT_CELL, this.selectCell);
|
||||
this.getScrollPosition();
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const { columns, getTableContentRect } = nextProps;
|
||||
const { width: tableContentWidth } = getTableContentRect();
|
||||
if (this.props.columns !== columns) {
|
||||
const columnMetrics = this.createColumnMetrics(nextProps);
|
||||
this.updateHorizontalScrollState({
|
||||
columnMetrics,
|
||||
scrollLeft: this.lastScrollLeft,
|
||||
gridWidth: tableContentWidth,
|
||||
});
|
||||
this.setState({ columnMetrics });
|
||||
} else if (this.props.getTableContentRect()?.width !== tableContentWidth) {
|
||||
this.updateHorizontalScrollState({
|
||||
columnMetrics: this.state.columnMetrics,
|
||||
scrollLeft: this.lastScrollLeft,
|
||||
gridWidth: tableContentWidth,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('copy', this.onCopyCells);
|
||||
document.removeEventListener('paste', this.onPasteCells);
|
||||
document.removeEventListener('cut', this.onCutCells);
|
||||
if (window.isMobile) {
|
||||
window.removeEventListener('touchstart', this.onTouchStart);
|
||||
window.removeEventListener('touchend', this.onTouchEnd);
|
||||
} else {
|
||||
document.removeEventListener('mousedown', this.onMouseDown);
|
||||
}
|
||||
|
||||
this.clearSetAbsoluteTimer();
|
||||
this.unsubscribeSelectNone();
|
||||
this.unsubscribeSelectCell();
|
||||
this.setState = (state, callback) => {
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
getNormalizedScroll = () => {
|
||||
const { scroll_left, scroll_top } = this.props.gridScroll || {};
|
||||
return {
|
||||
scroll_left: isNumber(scroll_left) ? scroll_left : 0,
|
||||
scroll_top: isNumber(scroll_top) ? scroll_top : 0,
|
||||
};
|
||||
};
|
||||
|
||||
getScrollPosition = () => {
|
||||
const { scroll_left, scroll_top } = this.getNormalizedScroll();
|
||||
if (this.bodyRef) {
|
||||
this.bodyRef.setScrollTop(scroll_top);
|
||||
this.setScrollLeft(scroll_left);
|
||||
this.handleHorizontalScroll(scroll_left, scroll_top);
|
||||
}
|
||||
};
|
||||
|
||||
storeScrollPosition = () => {
|
||||
if (this.props.storeGridScroll) {
|
||||
const scroll_top = this.bodyRef.getScrollTop();
|
||||
const scroll_left = this.getScrollLeft();
|
||||
this.props.storeGridScroll({ scroll_left, scroll_top });
|
||||
}
|
||||
};
|
||||
|
||||
createColumnMetrics = (props) => {
|
||||
const { columns, tableColumns, sequenceColumnWidth } = props;
|
||||
return recalculate(columns, tableColumns, sequenceColumnWidth);
|
||||
};
|
||||
|
||||
createRowMetrics = (props = this.props) => {
|
||||
return {
|
||||
idSelectedRecordMap: {},
|
||||
};
|
||||
};
|
||||
|
||||
setScrollLeft = (scrollLeft) => {
|
||||
this.resultContainerRef.scrollLeft = scrollLeft;
|
||||
};
|
||||
|
||||
onContentScroll = (e) => {
|
||||
const { scrollLeft } = e.target;
|
||||
const scrollTop = this.bodyRef.getScrollTop();
|
||||
const deltaX = this.scrollLeft - scrollLeft;
|
||||
const deltaY = this.scrollTop - scrollTop;
|
||||
this.scrollLeft = scrollLeft;
|
||||
if (deltaY !== 0) {
|
||||
this.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
// table horizontal scroll, set first column freeze
|
||||
if (deltaY === 0 && (deltaX !== 0 || scrollLeft === 0)) {
|
||||
this.handleHorizontalScroll(scrollLeft, scrollTop);
|
||||
}
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.CLOSE_EDITOR);
|
||||
};
|
||||
|
||||
handleHorizontalScroll = (scrollLeft, scrollTop) => {
|
||||
const { width: tableContentWidth } = this.props.getTableContentRect();
|
||||
if (isMobile) {
|
||||
this.updateHorizontalScrollState({
|
||||
scrollLeft,
|
||||
columnMetrics: this.state.columnMetrics,
|
||||
gridWidth: tableContentWidth,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// update classnames after scroll
|
||||
const originClassName = this.resultContainerRef ? this.resultContainerRef.className : '';
|
||||
let newClassName;
|
||||
if (scrollLeft > 0) {
|
||||
newClassName = addClassName(originClassName, 'horizontal-scroll');
|
||||
} else {
|
||||
newClassName = removeClassName(originClassName, 'horizontal-scroll');
|
||||
}
|
||||
if (newClassName !== originClassName && this.resultContainerRef) {
|
||||
this.resultContainerRef.className = newClassName;
|
||||
}
|
||||
|
||||
this.lastScrollLeft = scrollLeft;
|
||||
|
||||
this.handleFrozenDOMsPosition(scrollLeft, scrollTop);
|
||||
|
||||
if (this.recordsFooterRef && this.recordsFooterRef.setSummaryScrollLeft) {
|
||||
this.recordsFooterRef.setSummaryScrollLeft(scrollLeft);
|
||||
}
|
||||
|
||||
if (!this.isScrollByScrollbar) {
|
||||
this.handleScrollbarScroll(scrollLeft);
|
||||
}
|
||||
if (this.bodyRef && this.bodyRef.interactionMask) {
|
||||
this.bodyRef.setScrollLeft(scrollLeft, scrollTop);
|
||||
}
|
||||
|
||||
this.updateHorizontalScrollState({
|
||||
scrollLeft,
|
||||
columnMetrics: this.state.columnMetrics,
|
||||
gridWidth: tableContentWidth,
|
||||
});
|
||||
};
|
||||
|
||||
handleFrozenDOMsPosition = (scrollLeft, scrollTop) => {
|
||||
const { lastFrozenColumnKey } = this.state.columnMetrics;
|
||||
if (this.props.isGroupView && !lastFrozenColumnKey) {
|
||||
return; // none-frozen columns under group view
|
||||
}
|
||||
|
||||
this.clearSetAbsoluteTimer();
|
||||
this.setFixed(scrollLeft, scrollTop);
|
||||
this.timer = setTimeout(() => {
|
||||
this.setAbsolute(scrollLeft, scrollTop);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
handleScrollbarScroll = (scrollLeft) => {
|
||||
if (!this.horizontalScrollbar) return;
|
||||
if (!this.isScrollByScrollbar) {
|
||||
this.setHorizontalScrollbarScrollLeft(scrollLeft);
|
||||
return;
|
||||
}
|
||||
this.isScrollByScrollbar = false;
|
||||
};
|
||||
|
||||
onHorizontalScrollbarScroll = (scrollLeft) => {
|
||||
this.isScrollByScrollbar = true;
|
||||
this.setScrollLeft(scrollLeft);
|
||||
};
|
||||
|
||||
onHorizontalScrollbarMouseUp = () => {
|
||||
this.isScrollByScrollbar = false;
|
||||
};
|
||||
|
||||
setHorizontalScrollbarScrollLeft = (scrollLeft) => {
|
||||
this.horizontalScrollbar && this.horizontalScrollbar.setScrollLeft(scrollLeft);
|
||||
};
|
||||
|
||||
setFixed = (left, top) => {
|
||||
this.bodyRef.recordFrozenRefs.forEach(dom => {
|
||||
if (!dom) return;
|
||||
dom.frozenColumns.style.position = 'fixed';
|
||||
dom.frozenColumns.style.marginLeft = '0px';
|
||||
dom.frozenColumns.style.marginTop = '-' + top + 'px';
|
||||
});
|
||||
|
||||
if (this.bodyRef.fixFrozenDoms) {
|
||||
this.bodyRef.fixFrozenDoms(left, top);
|
||||
}
|
||||
};
|
||||
|
||||
setAbsolute = (left) => {
|
||||
const { isGroupView } = this.props;
|
||||
const { lastFrozenColumnKey } = this.state.columnMetrics;
|
||||
if (isGroupView && !lastFrozenColumnKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bodyRef.recordFrozenRefs.forEach(dom => {
|
||||
if (!dom) return;
|
||||
dom.frozenColumns.style.position = 'absolute';
|
||||
dom.frozenColumns.style.marginLeft = left + 'px';
|
||||
dom.frozenColumns.style.marginTop = '0px';
|
||||
});
|
||||
|
||||
if (this.bodyRef.cancelFixFrozenDOMs) {
|
||||
this.bodyRef.cancelFixFrozenDOMs(left);
|
||||
}
|
||||
|
||||
if (this.bodyRef && this.bodyRef.interactionMask) {
|
||||
this.bodyRef.cancelSetScrollLeft();
|
||||
}
|
||||
};
|
||||
|
||||
clearSetAbsoluteTimer = () => {
|
||||
if (!this.timer) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
};
|
||||
|
||||
getScrollLeft = () => {
|
||||
if (isMobile) {
|
||||
return 0;
|
||||
}
|
||||
return this.scrollLeft || 0;
|
||||
};
|
||||
|
||||
getScrollTop = () => {
|
||||
if (isMobile) {
|
||||
return 0;
|
||||
}
|
||||
return this.scrollTop || 0;
|
||||
};
|
||||
|
||||
setHorizontalScrollbarRef = (ref) => {
|
||||
this.horizontalScrollbar = ref;
|
||||
};
|
||||
|
||||
setResultContainerRef = (ref) => {
|
||||
this.resultContainerRef = ref;
|
||||
};
|
||||
|
||||
updateSelectedRange = (selectedRange) => {
|
||||
this.setState({ selectedRange });
|
||||
};
|
||||
|
||||
onClickContainer = (e) => {
|
||||
const classNames = getEventClassName(e);
|
||||
if (classNames.includes('sf-table-result-content')) {
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.CLOSE_EDITOR);
|
||||
}
|
||||
};
|
||||
|
||||
onCellClick = (cell) => {
|
||||
if (cell) {
|
||||
this.updateSelectedRange({
|
||||
topLeft: this.initPosition,
|
||||
bottomRight: this.initPosition,
|
||||
});
|
||||
}
|
||||
this.onDeselectAllRecords();
|
||||
};
|
||||
|
||||
onCellRangeSelectionUpdated = (selectedRange) => {
|
||||
this.onCellClick();
|
||||
this.updateSelectedRange(selectedRange);
|
||||
};
|
||||
|
||||
onCopyCells = (e) => {
|
||||
if (this.props.supportCopy) {
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.COPY_CELLS, e);
|
||||
}
|
||||
};
|
||||
|
||||
onPasteCells = (e) => {
|
||||
if (this.props.supportPaste) {
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.PASTE_CELLS, e);
|
||||
}
|
||||
};
|
||||
|
||||
onCutCells = (e) => {
|
||||
if (this.props.supportCut) {
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.CUT_CELLS, e);
|
||||
}
|
||||
};
|
||||
|
||||
onTouchStart = (e) => {
|
||||
const outsideDom = ['canvas', 'group-canvas'];
|
||||
if (e.target && outsideDom.includes(e.target.id)) {
|
||||
let touchStartPosition = {
|
||||
startX: e.changedTouches[0].clientX,
|
||||
startY: e.changedTouches[0].clientY,
|
||||
};
|
||||
this.setState({ touchStartPosition });
|
||||
}
|
||||
};
|
||||
|
||||
onTouchEnd = (e) => {
|
||||
const outsideDom = ['canvas', 'group-canvas'];
|
||||
if (e.target && outsideDom.includes(e.target.id)) {
|
||||
let { clientX, clientY } = e.changedTouches[0];
|
||||
let { touchStartPosition } = this.state;
|
||||
if (Math.abs(touchStartPosition.startX - clientX) < 5 && Math.abs(touchStartPosition.startY - clientY) < 5) {
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMouseDown = (e) => {
|
||||
const validClassName = getEventClassName(e);
|
||||
if (validClassName.indexOf('sf-table-cell') > -1) {
|
||||
return;
|
||||
}
|
||||
const outsideDom = ['canvas', 'group-canvas'];
|
||||
if (outsideDom.includes(e.target.id) || validClassName.includes('sf-table-result-content')) {
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_NONE);
|
||||
}
|
||||
};
|
||||
|
||||
selectNone = () => {
|
||||
this.setState({
|
||||
selectedRange: {
|
||||
topLeft: this.initPosition,
|
||||
bottomRight: this.initPosition
|
||||
},
|
||||
});
|
||||
|
||||
// clear selected records
|
||||
this.onDeselectAllRecords();
|
||||
};
|
||||
|
||||
selectCell = (cellPosition) => {
|
||||
this.setState({ selectedPosition: cellPosition });
|
||||
};
|
||||
|
||||
onSelectRecord = ({ groupRecordIndex, recordIndex }, e) => {
|
||||
e.stopPropagation();
|
||||
if (isShiftKeyDown(e)) {
|
||||
this.selectRecordWithShift({ groupRecordIndex, recordIndex });
|
||||
return;
|
||||
}
|
||||
const { isGroupView } = this.props;
|
||||
const { recordMetrics } = this.state;
|
||||
const operateRecord = this.props.recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex });
|
||||
if (!operateRecord) {
|
||||
return;
|
||||
}
|
||||
|
||||
const operateRecordId = operateRecord._id;
|
||||
if (RecordMetrics.isRecordSelected(operateRecordId, recordMetrics)) {
|
||||
this.deselectRecord(operateRecordId);
|
||||
this.setState({ lastRowIdxUiSelected: { groupRecordIndex: -1, recordIndex: -1 } });
|
||||
return;
|
||||
}
|
||||
this.selectRecord(operateRecordId);
|
||||
this.setState({ lastRowIdxUiSelected: { groupRecordIndex, recordIndex } });
|
||||
};
|
||||
|
||||
selectRecordWithShift = ({ groupRecordIndex, recordIndex }) => {
|
||||
const { recordIds, isGroupView } = this.props;
|
||||
const { lastRowIdxUiSelected, recordMetrics } = this.state;
|
||||
let selectedRecordIds = [];
|
||||
if (isGroupView) {
|
||||
if (!window.sfTableBody || !window.sfTableBody.getGroupMetrics) {
|
||||
return;
|
||||
}
|
||||
const groupMetrics = window.sfTableBody.getGroupMetrics();
|
||||
const { groupRows } = groupMetrics;
|
||||
const groupRecordIndexes = [groupRecordIndex, lastRowIdxUiSelected.groupRecordIndex].sort((a, b) => a - b);
|
||||
for (let i = groupRecordIndexes[0]; i <= groupRecordIndexes[1]; i++) {
|
||||
const groupRow = groupRows[i];
|
||||
const { type } = groupRow;
|
||||
if (type !== GROUP_ROW_TYPE.ROW) {
|
||||
continue;
|
||||
}
|
||||
selectedRecordIds.push(groupRow.rowId);
|
||||
}
|
||||
} else {
|
||||
const operateRecordId = recordIds[recordIndex];
|
||||
if (!operateRecordId) {
|
||||
return;
|
||||
}
|
||||
const lastSelectedRecordIndex = lastRowIdxUiSelected.recordIndex;
|
||||
if (lastSelectedRecordIndex < 0) {
|
||||
this.selectRecord(operateRecordId);
|
||||
this.setState({ lastRowIdxUiSelected: { groupRecordIndex: -1, recordIndex } });
|
||||
return;
|
||||
}
|
||||
if (recordIndex === lastSelectedRecordIndex || RecordMetrics.isRecordSelected(operateRecordId, recordMetrics)) {
|
||||
this.deselectRecord(operateRecordId);
|
||||
this.setState({ lastRowIdxUiSelected: { groupRecordIndex: -1, recordIndex: -1 } });
|
||||
return;
|
||||
}
|
||||
selectedRecordIds = this.getRecordIdsBetweenRange({ start: lastSelectedRecordIndex, end: recordIndex });
|
||||
}
|
||||
|
||||
if (selectedRecordIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.selectRecordsById(selectedRecordIds);
|
||||
this.setState({ lastRowIdxUiSelected: { groupRecordIndex, recordIndex } });
|
||||
};
|
||||
|
||||
getRecordIdsBetweenRange = ({ start, end }) => {
|
||||
const { recordIds: propsRecordIds } = this.props;
|
||||
const startIndex = Math.min(start, end);
|
||||
const endIndex = Math.max(start, end);
|
||||
let recordIds = [];
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
const recordId = propsRecordIds[i];
|
||||
if (recordId) {
|
||||
recordIds.push(recordId);
|
||||
}
|
||||
}
|
||||
return recordIds;
|
||||
};
|
||||
|
||||
selectRecord = (recordId) => {
|
||||
const { recordMetrics } = this.state;
|
||||
if (RecordMetrics.isRecordSelected(recordId, recordMetrics)) {
|
||||
return;
|
||||
}
|
||||
let updatedRecordMetrics = { ...recordMetrics };
|
||||
RecordMetrics.selectRecord(recordId, updatedRecordMetrics);
|
||||
this.setState({
|
||||
recordMetrics: updatedRecordMetrics,
|
||||
});
|
||||
};
|
||||
|
||||
selectRecordsById = (recordIds) => {
|
||||
const { recordMetrics } = this.state;
|
||||
const unSelectedRecordIds = recordIds.filter(recordId => !RecordMetrics.isRecordSelected(recordId, recordMetrics));
|
||||
if (unSelectedRecordIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
let updatedRecordMetrics = { ...recordMetrics };
|
||||
RecordMetrics.selectRecordsById(recordIds, updatedRecordMetrics);
|
||||
this.setState({
|
||||
recordMetrics: updatedRecordMetrics,
|
||||
});
|
||||
};
|
||||
|
||||
deselectRecord = (recordId) => {
|
||||
const { recordMetrics } = this.state;
|
||||
if (!RecordMetrics.isRecordSelected(recordId, recordMetrics)) {
|
||||
return;
|
||||
}
|
||||
let updatedRecordMetrics = { ...recordMetrics };
|
||||
RecordMetrics.deselectRecord(recordId, updatedRecordMetrics);
|
||||
this.setState({
|
||||
recordMetrics: updatedRecordMetrics,
|
||||
});
|
||||
};
|
||||
|
||||
selectAllRecords = () => {
|
||||
const { recordIds, isGroupView } = this.props;
|
||||
const { recordMetrics } = this.state;
|
||||
let updatedRecordMetrics = { ...recordMetrics };
|
||||
let selectedRowIds = [];
|
||||
if (isGroupView) {
|
||||
if (!window.sfTableBody || !window.sfTableBody.getGroupMetrics) {
|
||||
return;
|
||||
}
|
||||
const groupMetrics = window.sfTableBody.getGroupMetrics();
|
||||
const { groupRows } = groupMetrics;
|
||||
groupRows.forEach(groupRow => {
|
||||
const { type } = groupRow;
|
||||
if (type !== GROUP_ROW_TYPE.ROW) {
|
||||
return;
|
||||
}
|
||||
selectedRowIds.push(groupRow.rowId);
|
||||
});
|
||||
} else {
|
||||
selectedRowIds = recordIds;
|
||||
}
|
||||
RecordMetrics.selectRecordsById(selectedRowIds, updatedRecordMetrics);
|
||||
this.setState({
|
||||
recordMetrics: updatedRecordMetrics,
|
||||
});
|
||||
};
|
||||
|
||||
onDeselectAllRecords = () => {
|
||||
const { recordMetrics } = this.state;
|
||||
if (!RecordMetrics.hasSelectedRecords(recordMetrics)) {
|
||||
return;
|
||||
}
|
||||
let updatedRecordMetrics = { ...recordMetrics };
|
||||
RecordMetrics.deselectAllRecords(updatedRecordMetrics);
|
||||
this.setState({
|
||||
recordMetrics: updatedRecordMetrics,
|
||||
lastRowIdxUiSelected: { groupRecordIndex: -1, recordIndex: -1 },
|
||||
});
|
||||
};
|
||||
|
||||
hasSelectedCell = ({ groupRecordIndex, recordIndex }, selectedPosition) => {
|
||||
if (!selectedPosition) return false;
|
||||
const { isGroupView } = this.props;
|
||||
const { groupRecordIndex: selectedGroupRowIndex, rowIdx: selectedRecordIndex } = selectedPosition;
|
||||
if (isGroupView) {
|
||||
return groupRecordIndex === selectedGroupRowIndex;
|
||||
}
|
||||
return recordIndex === selectedRecordIndex;
|
||||
};
|
||||
|
||||
checkHasSelectedRecord = () => {
|
||||
const { recordMetrics } = this.state;
|
||||
if (!RecordMetrics.hasSelectedRecords(recordMetrics)) {
|
||||
return false;
|
||||
}
|
||||
const selectedRecordIds = RecordMetrics.getSelectedIds(recordMetrics);
|
||||
const selectedRecords = selectedRecordIds && selectedRecordIds.map(id => this.props.recordGetterById(id)).filter(Boolean);
|
||||
return selectedRecords && selectedRecords.length > 0;
|
||||
};
|
||||
|
||||
getHorizontalScrollState = ({ gridWidth, columnMetrics, scrollLeft }) => {
|
||||
const { columns } = columnMetrics;
|
||||
const columnsLength = columns.length;
|
||||
const { colVisibleStartIdx, colVisibleEndIdx } = getVisibleBoundaries(columns, scrollLeft, gridWidth);
|
||||
const colOverScanStartIdx = getColOverScanStartIdx(colVisibleStartIdx);
|
||||
const colOverScanEndIdx = getColOverScanEndIdx(colVisibleEndIdx, columnsLength);
|
||||
return {
|
||||
colOverScanStartIdx,
|
||||
colOverScanEndIdx,
|
||||
};
|
||||
};
|
||||
|
||||
updateHorizontalScrollState = ({ columnMetrics, gridWidth, scrollLeft }) => {
|
||||
const scrollState = this.getHorizontalScrollState({ columnMetrics, gridWidth, scrollLeft });
|
||||
this.setState(scrollState);
|
||||
};
|
||||
|
||||
isOutSelectedRange = ({ recordIndex, idx }) => {
|
||||
const { selectedRange } = this.state;
|
||||
const { topLeft, bottomRight } = selectedRange;
|
||||
const { idx: minIdx, rowIdx: minRowIdx } = topLeft;
|
||||
const { idx: maxIdx, rowIdx: maxRowIdx } = bottomRight;
|
||||
return idx < minIdx || idx > maxIdx || recordIndex < minRowIdx || recordIndex > maxRowIdx;
|
||||
};
|
||||
|
||||
onCellContextMenu = (cell) => {
|
||||
const { rowIdx: recordIndex, idx, groupRecordIndex } = cell;
|
||||
const { isGroupView, recordGetterByIndex } = this.props;
|
||||
const record = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex });
|
||||
|
||||
if (!record) return;
|
||||
const { recordMetrics } = this.state;
|
||||
const recordId = record._id;
|
||||
if (!RecordMetrics.isRecordSelected(recordId, recordMetrics)) {
|
||||
this.setState({ recordMetrics: this.createRowMetrics() });
|
||||
}
|
||||
|
||||
// select cell when click out of selectRange
|
||||
if (this.isOutSelectedRange({ recordIndex, idx })) {
|
||||
this.eventBus.dispatch(EVENT_BUS_TYPE.SELECT_CELL, cell, false);
|
||||
}
|
||||
};
|
||||
|
||||
getContainerWidth = () => {
|
||||
const { sequenceColumnWidth, groupOffsetLeft } = this.props;
|
||||
const { columnMetrics } = this.state;
|
||||
return sequenceColumnWidth + columnMetrics.totalWidth + CANVAS_RIGHT_INTERVAL + groupOffsetLeft;
|
||||
};
|
||||
|
||||
getTableCanvasContainerRect = () => {
|
||||
return this.resultContainerRef.getBoundingClientRect();
|
||||
};
|
||||
|
||||
getRecordsSummaries = () => {};
|
||||
|
||||
renderRecordsBody = ({ containerWidth }) => {
|
||||
const { recordMetrics, columnMetrics, colOverScanStartIdx, colOverScanEndIdx } = this.state;
|
||||
const { columns, allColumns, totalWidth, lastFrozenColumnKey, frozenColumnsWidth } = columnMetrics;
|
||||
const commonProps = {
|
||||
...this.props,
|
||||
columns, allColumns, totalWidth, lastFrozenColumnKey, frozenColumnsWidth,
|
||||
recordMetrics, colOverScanStartIdx, colOverScanEndIdx,
|
||||
contextMenu: (
|
||||
<ContextMenu
|
||||
{...this.props}
|
||||
recordMetrics={recordMetrics}
|
||||
/>
|
||||
),
|
||||
hasSelectedRecord: this.checkHasSelectedRecord(),
|
||||
getScrollLeft: this.getScrollLeft,
|
||||
getScrollTop: this.getScrollTop,
|
||||
selectNone: this.selectNone,
|
||||
onCellClick: this.onCellClick,
|
||||
onCellRangeSelectionUpdated: this.onCellRangeSelectionUpdated,
|
||||
onSelectRecord: this.onSelectRecord,
|
||||
setRecordsScrollLeft: this.setScrollLeft,
|
||||
storeScrollPosition: this.storeScrollPosition,
|
||||
hasSelectedCell: this.hasSelectedCell,
|
||||
onCellContextMenu: this.onCellContextMenu,
|
||||
getTableCanvasContainerRect: this.getTableCanvasContainerRect,
|
||||
};
|
||||
if (this.props.isGroupView) {
|
||||
return (
|
||||
<GroupBody
|
||||
onRef={ref => this.bodyRef = ref}
|
||||
{...commonProps}
|
||||
containerWidth={containerWidth}
|
||||
groups={this.props.groups}
|
||||
groupbys={this.props.groupbys}
|
||||
groupOffsetLeft={this.props.groupOffsetLeft}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Body
|
||||
onRef={ref => this.bodyRef = ref}
|
||||
{...commonProps}
|
||||
recordIds={this.props.recordIds}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
recordIds, recordsCount, showSequenceColumn, sequenceColumnWidth, isGroupView, groupOffsetLeft,
|
||||
} = this.props;
|
||||
const { recordMetrics, columnMetrics, selectedRange, colOverScanStartIdx, colOverScanEndIdx } = this.state;
|
||||
const { columns, totalWidth, lastFrozenColumnKey } = columnMetrics;
|
||||
const containerWidth = this.getContainerWidth();
|
||||
const hasSelectedRecord = this.checkHasSelectedRecord();
|
||||
const isSelectedAll = showSequenceColumn && RecordMetrics.isSelectedAll(recordIds, recordMetrics);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`sf-table-result-container ${this.isWindows ? 'windows-browser' : ''}`}
|
||||
ref={this.setResultContainerRef}
|
||||
onScroll={this.onContentScroll}
|
||||
onClick={this.onClickContainer}
|
||||
>
|
||||
<div className="sf-table-result-content" style={{ width: containerWidth }}>
|
||||
<RecordsHeader
|
||||
onRef={(ref) => this.headerFrozenRef = ref}
|
||||
containerWidth={containerWidth}
|
||||
ColumnDropdownMenu={this.props.ColumnDropdownMenu}
|
||||
NewColumnComponent={this.props.NewColumnComponent}
|
||||
headerSettings={this.props.headerSettings}
|
||||
columnMetrics={columnMetrics}
|
||||
colOverScanStartIdx={colOverScanStartIdx}
|
||||
colOverScanEndIdx={colOverScanEndIdx}
|
||||
hasSelectedRecord={hasSelectedRecord}
|
||||
showSequenceColumn={showSequenceColumn}
|
||||
sequenceColumnWidth={sequenceColumnWidth}
|
||||
isSelectedAll={isSelectedAll}
|
||||
isGroupView={isGroupView}
|
||||
groupOffsetLeft={groupOffsetLeft}
|
||||
lastFrozenColumnKey={lastFrozenColumnKey}
|
||||
selectNoneRecords={this.selectNone}
|
||||
selectAllRecords={this.selectAllRecords}
|
||||
modifyColumnOrder={this.props.modifyColumnOrder}
|
||||
modifyColumnWidth={this.props.modifyColumnWidth}
|
||||
insertColumn={this.props.insertColumn}
|
||||
/>
|
||||
{this.renderRecordsBody({ containerWidth })}
|
||||
</div>
|
||||
</div>
|
||||
{this.isWindows && this.isWebkit && (
|
||||
<HorizontalScrollbar
|
||||
ref={this.setHorizontalScrollbarRef}
|
||||
innerWidth={totalWidth + CANVAS_RIGHT_INTERVAL}
|
||||
onScrollbarScroll={this.onHorizontalScrollbarScroll}
|
||||
onScrollbarMouseUp={this.onHorizontalScrollbarMouseUp}
|
||||
/>
|
||||
)}
|
||||
{this.props.showGridFooter &&
|
||||
<RecordsFooter
|
||||
ref={ref => this.recordsFooterRef = ref}
|
||||
recordsCount={recordsCount}
|
||||
hasMoreRecords={this.props.hasMoreRecords}
|
||||
columns={columns}
|
||||
sequenceColumnWidth={sequenceColumnWidth}
|
||||
groupOffsetLeft={groupOffsetLeft}
|
||||
recordMetrics={recordMetrics}
|
||||
selectedRange={selectedRange}
|
||||
isGroupView={isGroupView}
|
||||
hasSelectedRecord={hasSelectedRecord}
|
||||
isLoadingMoreRecords={this.props.isLoadingMoreRecords}
|
||||
recordGetterById={this.props.recordGetterById}
|
||||
recordGetterByIndex={this.props.recordGetterByIndex}
|
||||
getRecordsSummaries={this.getRecordsSummaries}
|
||||
loadAll={this.props.loadAll}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Records.propTypes = {
|
||||
tableId: PropTypes.string,
|
||||
tableColumns: PropTypes.array,
|
||||
columns: PropTypes.array,
|
||||
columnEditable: PropTypes.bool,
|
||||
ColumnDropdownMenu: PropTypes.object,
|
||||
NewColumnComponent: PropTypes.object,
|
||||
headerSettings: PropTypes.object,
|
||||
showSequenceColumn: PropTypes.bool,
|
||||
sequenceColumnWidth: PropTypes.number,
|
||||
hasMoreRecords: PropTypes.bool,
|
||||
isLoadingMoreRecords: PropTypes.bool,
|
||||
isGroupView: PropTypes.bool,
|
||||
groupOffsetLeft: PropTypes.number,
|
||||
recordIds: PropTypes.array,
|
||||
recordsCount: PropTypes.number,
|
||||
groups: PropTypes.array,
|
||||
groupbys: PropTypes.array,
|
||||
searchResult: PropTypes.object,
|
||||
showGridFooter: PropTypes.bool,
|
||||
supportCopy: PropTypes.bool,
|
||||
supportCut: PropTypes.bool,
|
||||
supportPaste: PropTypes.bool,
|
||||
gridScroll: PropTypes.object,
|
||||
getTableContentRect: PropTypes.func,
|
||||
storeGridScroll: PropTypes.func,
|
||||
scrollToLoadMore: PropTypes.func,
|
||||
updateRecord: PropTypes.func,
|
||||
recordGetterById: PropTypes.func,
|
||||
recordGetterByIndex: PropTypes.func,
|
||||
loadAll: PropTypes.func,
|
||||
insertColumn: PropTypes.func,
|
||||
modifyColumnWidth: PropTypes.func,
|
||||
modifyColumnOrder: PropTypes.func,
|
||||
getUpdateDraggedRecords: PropTypes.func,
|
||||
getCopiedRecordsAndColumnsFromRange: PropTypes.func,
|
||||
moveRecord: PropTypes.func,
|
||||
addFolder: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Records;
|
@@ -0,0 +1,35 @@
|
||||
.sf-table-row .rdg-row-expand-icon {
|
||||
opacity: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sf-table-row:hover .rdg-row-expand-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sf-table-row .sf-table-column-content.actions-checkbox {
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sf-table-row:hover .sf-table-column-content.actions-checkbox {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sf-table-row:hover .sf-table-column-content.row-index {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sf-table-row.row-selected .sf-table-column-content.actions-checkbox {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.sf-table-row.row-selected .sf-table-column-content.row-index {
|
||||
display: none !important;
|
||||
}
|
@@ -0,0 +1,105 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Tooltip } from 'reactstrap';
|
||||
import { isMobile } from '../../../../../../utils/utils';
|
||||
import { gettext } from '../../../../../../utils/constants';
|
||||
import { SEQUENCE_COLUMN_WIDTH } from '../../../../constants/grid';
|
||||
|
||||
import './index.css';
|
||||
|
||||
class ActionsCell extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isLockedRowTooltipShow: false,
|
||||
};
|
||||
}
|
||||
|
||||
onCellMouseEnter = () => {
|
||||
const { isLocked } = this.props;
|
||||
if (!isLocked || isMobile) return;
|
||||
this.timer = setTimeout(() => {
|
||||
this.setState({ isLockedRowTooltipShow: true });
|
||||
}, 500);
|
||||
};
|
||||
|
||||
onCellMouseLeave = () => {
|
||||
const { isLocked } = this.props;
|
||||
if (!isLocked || isMobile) return;
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
this.setState({ isLockedRowTooltipShow: false });
|
||||
};
|
||||
|
||||
getLockedRowTooltip = () => {
|
||||
const { recordId } = this.props;
|
||||
return (
|
||||
<Tooltip
|
||||
target={`action-cell-${recordId}`}
|
||||
placement='bottom'
|
||||
isOpen={this.state.isLockedRowTooltipShow}
|
||||
fade={false}
|
||||
hideArrow={true}
|
||||
className="readonly-cell-tooltip"
|
||||
>
|
||||
{gettext('The row is locked and cannot be modified')}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isSelected, isLastFrozenCell, index, height, recordId } = this.props;
|
||||
const cellStyle = {
|
||||
height,
|
||||
width: SEQUENCE_COLUMN_WIDTH,
|
||||
minWidth: SEQUENCE_COLUMN_WIDTH,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={classnames('sf-table-cell column actions-cell', { 'table-last--frozen': isLastFrozenCell })}
|
||||
id={`action-cell-${recordId}`}
|
||||
style={{ ...cellStyle }}
|
||||
onMouseEnter={this.onCellMouseEnter}
|
||||
onMouseLeave={this.onCellMouseLeave}
|
||||
>
|
||||
{!isSelected && <div className="sf-table-column-content row-index text-truncate">{index + 1}</div>}
|
||||
<div className="sf-table-column-content actions-checkbox">
|
||||
<div className="select-cell-checkbox-container" onClick={this.props.onSelectRecord}>
|
||||
<input
|
||||
id={`select-cell-checkbox-${recordId}`}
|
||||
className="select-cell-checkbox"
|
||||
type='checkbox'
|
||||
name='row-selection'
|
||||
checked={isSelected || false}
|
||||
readOnly
|
||||
/>
|
||||
<label
|
||||
htmlFor={`select-cell-checkbox-${recordId}`}
|
||||
name={gettext('Select')}
|
||||
title={gettext('Select')}
|
||||
aria-label={gettext('Select')}
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/* {this.getLockedRowTooltip()} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ActionsCell.propTypes = {
|
||||
isLocked: PropTypes.bool,
|
||||
isSelected: PropTypes.bool,
|
||||
isLastFrozenCell: PropTypes.bool,
|
||||
recordId: PropTypes.string,
|
||||
index: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
onSelectRecord: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionsCell;
|
@@ -0,0 +1,168 @@
|
||||
.sf-table-cell {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border-right: 1px solid #eee;
|
||||
padding: 6px 8px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.sf-table-cell:not(.table-cell-uneditable):hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sf-table-cell.index {
|
||||
width: 90px;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sf-table-cell.column {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.sf-table-cell .select-cell-checkbox-container,
|
||||
.sf-table-cell .select-all-checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sf-table-cell .select-cell-checkbox-container:hover,
|
||||
.sf-table-cell .select-all-checkbox-container:hover,
|
||||
.sf-table-cell .select-cell-checkbox-container input:hover,
|
||||
.sf-table-cell .select-all-checkbox-container input:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sf-table-cell .select-cell-checkbox {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.sf-table-cell .cell-file-add {
|
||||
height: 31px;
|
||||
line-height: 31px;
|
||||
text-align: center;
|
||||
color: #666666;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
top: -5px;
|
||||
left: -8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sf-table-cell .file-cell-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* cell formatter */
|
||||
.sf-table-cell .sf-metadata-ui.cell-formatter-container {
|
||||
height: 100%;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sf-table-cell .collaborator-avatar {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-left: 0;
|
||||
transform: translateY(0px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sf-table-cell .select-all-checkbox-container .sf-metadata-icon-partially-selected {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
fill: #2b76f6;
|
||||
}
|
||||
|
||||
.sf-table-cell .header-action-cell-placeholder {
|
||||
/* same width as the button of record expand */
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.sf-table-cell .mobile-select-all-container {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
display: inline-flex;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sf-table-cell .mobile-select-all-container .mobile-select-all-checkbox {
|
||||
display: none;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.sf-table-cell .mobile-select-all-container .select-all-checkbox-show {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.sf-table-cell .mobile-select-all-container .mobile-select-all-checkbox:checked + .select-all-checkbox-show {
|
||||
border: unset;
|
||||
background-color: #3b88fd;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sf-table-cell .mobile-select-all-container .mobile-select-all-checkbox:checked + .select-all-checkbox-show::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 4px;
|
||||
width: 5px;
|
||||
height: 8px;
|
||||
border: solid white;
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.sf-table-cell .mobile-select-all-container .sf-metadata-icon-partially-selected {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 991.8px) {
|
||||
.sf-table-cell .select-cell-checkbox {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* row cell ui */
|
||||
.sf-table-row:hover .sf-table-cell,
|
||||
.sf-table-row:hover .column {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.sf-table-row:hover .cell-selected {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.sf-table-records-wrapper:hover .sf-table-cell.cell-highlight {
|
||||
background-color: rgb(239, 199, 151) !important;
|
||||
}
|
||||
|
||||
.sf-table-records-wrapper:hover .sf-table-cell.cell-current-highlight {
|
||||
background-color: rgb(240, 159, 63) !important;
|
||||
}
|
||||
|
||||
.sf-table-row.row-selected .sf-table-cell,
|
||||
.sf-table-row.row-selected .column {
|
||||
background-color: #dbecfa !important;
|
||||
}
|
@@ -0,0 +1,189 @@
|
||||
import React, { cloneElement, isValidElement, useCallback, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { Utils } from '../../../../../../utils/utils';
|
||||
import { getCellValueByColumn } from '../../../../utils/cell';
|
||||
import { cellCompare, checkCellValueChanged } from '../../../../utils/cell-comparer';
|
||||
import { checkIsColumnEditable } from '../../../../utils/column';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const Cell = React.memo(({
|
||||
needBindEvents,
|
||||
column,
|
||||
record,
|
||||
groupRecordIndex,
|
||||
recordIndex,
|
||||
cellMetaData,
|
||||
highlightClassName,
|
||||
isLastCell,
|
||||
isLastFrozenCell,
|
||||
isCellSelected,
|
||||
bgColor,
|
||||
frozen,
|
||||
height,
|
||||
checkCanModifyRecord,
|
||||
}) => {
|
||||
const cellEditable = useMemo(() => {
|
||||
return checkIsColumnEditable(column) && checkCanModifyRecord && checkCanModifyRecord(record);
|
||||
}, [column, record, checkCanModifyRecord]);
|
||||
|
||||
const className = useMemo(() => {
|
||||
const { type } = column;
|
||||
return classnames('sf-table-cell', `sf-table-${type}-cell`, highlightClassName, {
|
||||
'table-cell-uneditable': !cellEditable,
|
||||
'last-cell': isLastCell,
|
||||
'table-last--frozen': isLastFrozenCell,
|
||||
'cell-selected': isCellSelected,
|
||||
// 'dragging-file-to-cell': ,
|
||||
// 'row-comment-cell': ,
|
||||
});
|
||||
}, [cellEditable, column, highlightClassName, isLastCell, isLastFrozenCell, isCellSelected]);
|
||||
|
||||
const style = useMemo(() => {
|
||||
const { left, width } = column;
|
||||
let value = {
|
||||
width,
|
||||
height,
|
||||
};
|
||||
if (!frozen) {
|
||||
value.left = left;
|
||||
}
|
||||
if (bgColor) {
|
||||
value.backgroundColor = bgColor;
|
||||
}
|
||||
return value;
|
||||
}, [frozen, height, column, bgColor]);
|
||||
|
||||
const onCellClick = useCallback((event) => {
|
||||
const cell = { idx: column.idx, groupRecordIndex, rowIdx: recordIndex };
|
||||
|
||||
// select cell
|
||||
if (Utils.isFunction(cellMetaData.onCellClick)) {
|
||||
cellMetaData.onCellClick(cell, event);
|
||||
}
|
||||
}, [column, groupRecordIndex, recordIndex, cellMetaData]);
|
||||
|
||||
const onCellDoubleClick = useCallback((event) => {
|
||||
if (!Utils.isFunction(cellMetaData.onCellDoubleClick)) return;
|
||||
const cell = { idx: column.idx, groupRecordIndex, rowIdx: recordIndex };
|
||||
cellMetaData.onCellDoubleClick(cell, event);
|
||||
}, [column, groupRecordIndex, recordIndex, cellMetaData]);
|
||||
|
||||
const onCellMouseDown = useCallback((event) => {
|
||||
if (event.button === 2) return;
|
||||
if (!Utils.isFunction(cellMetaData.onCellMouseDown)) return;
|
||||
const cell = { idx: column.idx, groupRecordIndex, rowIdx: recordIndex };
|
||||
cellMetaData.onCellMouseDown(cell, event);
|
||||
}, [column, groupRecordIndex, recordIndex, cellMetaData]);
|
||||
|
||||
const onCellMouseEnter = useCallback((event) => {
|
||||
if (!Utils.isFunction(cellMetaData.onCellMouseEnter)) return;
|
||||
const cell = { idx: column.idx, groupRecordIndex, rowIdx: recordIndex };
|
||||
const mousePosition = { x: event.clientX, y: event.clientY };
|
||||
cellMetaData.onCellMouseEnter({ ...cell, mousePosition }, event);
|
||||
}, [column, groupRecordIndex, recordIndex, cellMetaData]);
|
||||
|
||||
const onCellMouseMove = useCallback((event) => {
|
||||
if (!Utils.isFunction(cellMetaData.onCellMouseMove)) return;
|
||||
const cell = { idx: column.idx, groupRecordIndex, rowIdx: recordIndex };
|
||||
const mousePosition = { x: event.clientX, y: event.clientY };
|
||||
cellMetaData.onCellMouseMove({ ...cell, mousePosition }, event);
|
||||
}, [column, groupRecordIndex, recordIndex, cellMetaData]);
|
||||
|
||||
const onCellMouseLeave = useCallback(() => {
|
||||
return;
|
||||
}, []);
|
||||
|
||||
const onDragOver = useCallback((event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}, []);
|
||||
|
||||
const onCellContextMenu = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const cell = { idx: column.idx, groupRecordIndex, rowIdx: recordIndex };
|
||||
if (!Utils.isFunction(cellMetaData.onCellContextMenu)) return;
|
||||
cellMetaData.onCellContextMenu(cell);
|
||||
}, [cellMetaData, column, groupRecordIndex, recordIndex]);
|
||||
|
||||
const getEvents = useCallback(() => {
|
||||
return {
|
||||
onClick: onCellClick,
|
||||
onDoubleClick: onCellDoubleClick,
|
||||
onMouseDown: onCellMouseDown,
|
||||
onMouseEnter: onCellMouseEnter,
|
||||
onMouseMove: onCellMouseMove,
|
||||
onMouseLeave: onCellMouseLeave,
|
||||
onDragOver: onDragOver,
|
||||
onContextMenu: onCellContextMenu,
|
||||
};
|
||||
}, [onCellClick, onCellDoubleClick, onCellMouseDown, onCellMouseEnter, onCellMouseMove, onCellMouseLeave, onDragOver, onCellContextMenu]);
|
||||
|
||||
const getOldRowData = useCallback((originalOldCellValue) => {
|
||||
const { key: columnKey, name: columnName, is_private } = column;
|
||||
const oldRowData = is_private ? { [columnKey]: originalOldCellValue } : { [columnName]: originalOldCellValue };
|
||||
const originalOldRowData = { [columnKey]: originalOldCellValue }; // { [column.key]: cellValue }
|
||||
return { oldRowData, originalOldRowData };
|
||||
}, [column]);
|
||||
|
||||
const modifyRecord = useCallback((updated) => {
|
||||
if (!Utils.isFunction(cellMetaData.modifyRecord)) return;
|
||||
const { key: columnKey, name: columnName, is_private } = column;
|
||||
const originalOldCellValue = getCellValueByColumn(record, column);
|
||||
const newCellValue = updated[columnKey];
|
||||
if (!checkCellValueChanged(originalOldCellValue, newCellValue)) return;
|
||||
const rowId = record._id;
|
||||
const key = Object.keys(updated)[0];
|
||||
let updates = updated;
|
||||
if (!is_private) {
|
||||
updates = { [columnName]: updated[key] };
|
||||
}
|
||||
const { oldRowData, originalOldRowData } = getOldRowData(originalOldCellValue);
|
||||
// updates used for update remote record data
|
||||
// originalUpdates used for update local record data
|
||||
// oldRowData ues for undo/undo modify record
|
||||
// originalOldRowData ues for undo/undo modify record
|
||||
cellMetaData.modifyRecord({ rowId, cellKey: columnKey, updates, originalUpdates: updated, oldRowData, originalOldRowData });
|
||||
}, [cellMetaData, record, column, getOldRowData]);
|
||||
|
||||
const cellValue = getCellValueByColumn(record, column);
|
||||
const cellEvents = needBindEvents && getEvents();
|
||||
const containerProps = {
|
||||
className,
|
||||
style,
|
||||
...cellEvents,
|
||||
};
|
||||
return (
|
||||
<div key={`${record._id}-${column.key}`} {...containerProps}>
|
||||
{isValidElement(column.formatter) && cloneElement(column.formatter, { isCellSelected, value: cellValue, column, record, onChange: modifyRecord })}
|
||||
{(isCellSelected && isValidElement(cellMetaData.CellOperationBtn)) && (cloneElement(cellMetaData.CellOperationBtn, { record, column }))}
|
||||
</div>
|
||||
);
|
||||
}, (props, nextProps) => {
|
||||
return !cellCompare(props, nextProps);
|
||||
});
|
||||
|
||||
Cell.defaultProps = {
|
||||
needBindEvents: true
|
||||
};
|
||||
|
||||
Cell.propTypes = {
|
||||
frozen: PropTypes.bool,
|
||||
isCellSelected: PropTypes.bool,
|
||||
isLastCell: PropTypes.bool,
|
||||
isLastFrozenCell: PropTypes.bool,
|
||||
cellMetaData: PropTypes.object,
|
||||
record: PropTypes.object.isRequired,
|
||||
groupRecordIndex: PropTypes.number,
|
||||
recordIndex: PropTypes.number.isRequired,
|
||||
column: PropTypes.object.isRequired,
|
||||
height: PropTypes.number,
|
||||
needBindEvents: PropTypes.bool,
|
||||
modifyRecord: PropTypes.func,
|
||||
highlightClassName: PropTypes.string,
|
||||
bgColor: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Cell;
|
@@ -0,0 +1,26 @@
|
||||
.sf-table-row .cell-jump-link {
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
margin-left: 8px;
|
||||
border: 1px solid #eee;
|
||||
padding: 0 2px;
|
||||
color: #666666;
|
||||
border-radius: 2px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 1px;
|
||||
}
|
||||
|
||||
.cell-highlight {
|
||||
background-color: rgb(239, 199, 151) !important;
|
||||
}
|
||||
|
||||
.cell-current-highlight {
|
||||
background-color: #f09f3f !important;
|
||||
}
|
||||
|
||||
.frozen-columns {
|
||||
background-color: #fff;
|
||||
}
|
@@ -0,0 +1,302 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import Cell from './cell';
|
||||
import ActionsCell from './actions-cell';
|
||||
import { getFrozenColumns } from '../../../utils/column';
|
||||
import { SEQUENCE_COLUMN as Z_INDEX_SEQUENCE_COLUMN, FROZEN_GROUP_CELL as Z_INDEX_FROZEN_GROUP_CELL } from '../../../constants/z-index';
|
||||
|
||||
import './index.css';
|
||||
|
||||
class Record extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.checkScroll();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return (
|
||||
nextProps.isGroupView !== this.props.isGroupView ||
|
||||
nextProps.hasSelectedCell !== this.props.hasSelectedCell ||
|
||||
(nextProps.hasSelectedCell && this.props.selectedPosition.idx !== nextProps.selectedPosition.idx) || // selected cell in same row but different column
|
||||
nextProps.isSelected !== this.props.isSelected ||
|
||||
nextProps.groupRecordIndex !== this.props.groupRecordIndex ||
|
||||
nextProps.index !== this.props.index ||
|
||||
nextProps.isLastRecord !== this.props.isLastRecord ||
|
||||
nextProps.lastFrozenColumnKey !== this.props.lastFrozenColumnKey ||
|
||||
nextProps.columns !== this.props.columns ||
|
||||
nextProps.showSequenceColumn !== this.props.showSequenceColumn ||
|
||||
nextProps.sequenceColumnWidth !== this.props.sequenceColumnWidth ||
|
||||
nextProps.colOverScanStartIdx !== this.props.colOverScanStartIdx ||
|
||||
nextProps.colOverScanEndIdx !== this.props.colOverScanEndIdx ||
|
||||
nextProps.record !== this.props.record ||
|
||||
nextProps.top !== this.props.top ||
|
||||
nextProps.left !== this.props.left ||
|
||||
nextProps.height !== this.props.height ||
|
||||
nextProps.searchResult !== this.props.searchResult ||
|
||||
nextProps.columnColor !== this.props.columnColor
|
||||
);
|
||||
}
|
||||
|
||||
checkScroll = () => {
|
||||
this.cancelFixFrozenDOMs(this.props.scrollLeft);
|
||||
};
|
||||
|
||||
cancelFixFrozenDOMs = (scrollLeft) => {
|
||||
const { isGroupView } = this.props;
|
||||
const frozenChildrenCount = this.frozenColumns.childElementCount;
|
||||
if (!this.frozenColumns || frozenChildrenCount < 1 || (isGroupView && frozenChildrenCount < 2)) {
|
||||
return;
|
||||
}
|
||||
this.frozenColumns.style.position = 'absolute';
|
||||
this.frozenColumns.style.marginLeft = scrollLeft + 'px';
|
||||
this.frozenColumns.style.marginTop = '0px';
|
||||
};
|
||||
|
||||
onSelectRecord = (e) => {
|
||||
const { groupRecordIndex, index } = this.props;
|
||||
this.props.selectNoneCells();
|
||||
this.props.onSelectRecord({ groupRecordIndex, recordIndex: index }, e);
|
||||
};
|
||||
|
||||
checkIsCellSelected = (columnIdx) => {
|
||||
const { hasSelectedCell, selectedPosition } = this.props;
|
||||
if (!selectedPosition) return false;
|
||||
return hasSelectedCell && selectedPosition.idx === columnIdx;
|
||||
};
|
||||
|
||||
checkIsLastCell(columns, columnKey) {
|
||||
return columns[columns.length - 1].key === columnKey;
|
||||
}
|
||||
|
||||
reloadCurrentRecord = () => {
|
||||
this.props.reloadRecords([this.props.record._id]);
|
||||
};
|
||||
|
||||
getFrozenCells = () => {
|
||||
const {
|
||||
columns, sequenceColumnWidth, lastFrozenColumnKey, groupRecordIndex, index: recordIndex, record,
|
||||
cellMetaData, isGroupView, height, columnColor
|
||||
} = this.props;
|
||||
const frozenColumns = getFrozenColumns(columns);
|
||||
if (frozenColumns.length === 0) return null;
|
||||
const recordId = record._id;
|
||||
return frozenColumns.map((column, index) => {
|
||||
const { key } = column;
|
||||
const isCellHighlight = this.checkIsCellHighlight(key, recordId);
|
||||
const isCurrentCellHighlight = this.checkIsCurrentCellHighlight(key, recordId);
|
||||
const highlightClassName = isCurrentCellHighlight ? 'cell-current-highlight' : isCellHighlight ? 'cell-highlight' : null;
|
||||
const isCellSelected = this.checkIsCellSelected(index);
|
||||
const isLastCell = this.checkIsLastCell(columns, key);
|
||||
const isLastFrozenCell = key === lastFrozenColumnKey;
|
||||
const bgColor = columnColor && columnColor[key];
|
||||
return (
|
||||
<Cell
|
||||
frozen
|
||||
key={column.key}
|
||||
record={record}
|
||||
groupRecordIndex={groupRecordIndex}
|
||||
recordIndex={recordIndex}
|
||||
isCellSelected={isCellSelected}
|
||||
isLastCell={isLastCell}
|
||||
isLastFrozenCell={isLastFrozenCell}
|
||||
height={isGroupView ? height : height - 1}
|
||||
column={column}
|
||||
sequenceColumnWidth={sequenceColumnWidth}
|
||||
cellMetaData={cellMetaData}
|
||||
checkCanModifyRecord={this.props.checkCanModifyRecord}
|
||||
checkCellValueChanged={this.props.checkCellValueChanged}
|
||||
reloadCurrentRecord={this.reloadCurrentRecord}
|
||||
highlightClassName={highlightClassName}
|
||||
bgColor={bgColor}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
checkIsCellHighlight = (columnKey, rowId) => {
|
||||
const { searchResult } = this.props;
|
||||
if (searchResult) {
|
||||
const matchedColumns = searchResult.matchedRows[rowId];
|
||||
if (matchedColumns && matchedColumns.includes(columnKey)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
checkIsCurrentCellHighlight = (columnKey, rowId) => {
|
||||
const { searchResult } = this.props;
|
||||
if (searchResult) {
|
||||
const { currentSelectIndex } = searchResult;
|
||||
if (typeof(currentSelectIndex) !== 'number') return false;
|
||||
const currentSelectCell = searchResult.matchedCells[currentSelectIndex];
|
||||
if (!currentSelectCell) return false;
|
||||
if (currentSelectCell.row === rowId && currentSelectCell.column === columnKey) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
getColumnCells = () => {
|
||||
const {
|
||||
columns, sequenceColumnWidth, colOverScanStartIdx, colOverScanEndIdx, groupRecordIndex, index: recordIndex,
|
||||
record, cellMetaData, isGroupView, height, columnColor,
|
||||
} = this.props;
|
||||
const recordId = record._id;
|
||||
const rendererColumns = columns.slice(colOverScanStartIdx, colOverScanEndIdx);
|
||||
return rendererColumns.map((column) => {
|
||||
const { key, frozen } = column;
|
||||
const needBindEvents = !frozen;
|
||||
const isCellSelected = this.checkIsCellSelected(columns.findIndex(col => col.key === column.key));
|
||||
const isCellHighlight = this.checkIsCellHighlight(key, recordId);
|
||||
const isCurrentCellHighlight = this.checkIsCurrentCellHighlight(key, recordId);
|
||||
const highlightClassName = isCurrentCellHighlight ? 'cell-current-highlight' : isCellHighlight ? 'cell-highlight' : null;
|
||||
const isLastCell = this.checkIsLastCell(columns, key);
|
||||
const bgColor = columnColor && columnColor[key];
|
||||
return (
|
||||
<Cell
|
||||
key={column.key}
|
||||
record={record}
|
||||
groupRecordIndex={groupRecordIndex}
|
||||
recordIndex={recordIndex}
|
||||
isCellSelected={isCellSelected}
|
||||
isLastCell={isLastCell}
|
||||
height={isGroupView ? height : height - 1}
|
||||
column={column}
|
||||
sequenceColumnWidth={sequenceColumnWidth}
|
||||
needBindEvents={needBindEvents}
|
||||
cellMetaData={cellMetaData}
|
||||
checkCanModifyRecord={this.props.checkCanModifyRecord}
|
||||
checkCellValueChanged={this.props.checkCellValueChanged}
|
||||
reloadCurrentRecord={this.reloadCurrentRecord}
|
||||
highlightClassName={highlightClassName}
|
||||
bgColor={bgColor}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
getRecordStyle = () => {
|
||||
const { isGroupView, height } = this.props;
|
||||
let style = { height };
|
||||
if (isGroupView) {
|
||||
const { top, left } = this.props;
|
||||
style.top = top;
|
||||
style.left = left;
|
||||
}
|
||||
return style;
|
||||
};
|
||||
|
||||
getFrozenColumnsStyle = () => {
|
||||
const { isGroupView, lastFrozenColumnKey, height } = this.props;
|
||||
let style = {
|
||||
zIndex: Z_INDEX_SEQUENCE_COLUMN,
|
||||
height: height - 1,
|
||||
};
|
||||
if (isGroupView) {
|
||||
style.height = height;
|
||||
style.zIndex = Z_INDEX_FROZEN_GROUP_CELL;
|
||||
if (!lastFrozenColumnKey) {
|
||||
style.marginLeft = '0px';
|
||||
}
|
||||
}
|
||||
return style;
|
||||
};
|
||||
|
||||
// handle drag copy
|
||||
handleDragEnter = (e) => {
|
||||
// Prevent default to allow drop
|
||||
e.preventDefault();
|
||||
const { index, groupRecordIndex, cellMetaData: { onDragEnter } } = this.props;
|
||||
onDragEnter({ overRecordIdx: index, overGroupRecordIndex: groupRecordIndex });
|
||||
};
|
||||
|
||||
handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
};
|
||||
|
||||
handleDrop = (e) => {
|
||||
// The default in Firefox is to treat data in dataTransfer as a URL and perform navigation on it, even if the data type used is 'text'
|
||||
// To bypass this, we need to capture and prevent the drop event.
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
isSelected, isGroupView, showSequenceColumn, index, isLastRecord, lastFrozenColumnKey, height, record
|
||||
} = this.props;
|
||||
const isLocked = record._locked ? true : false;
|
||||
const cellHeight = isGroupView ? height : height - 1;
|
||||
|
||||
const frozenCells = this.getFrozenCells();
|
||||
const columnCells = this.getColumnCells();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('sf-table-row', {
|
||||
'sf-table-last-row': isLastRecord,
|
||||
'row-selected': isSelected,
|
||||
'row-locked': isLocked
|
||||
})}
|
||||
style={this.getRecordStyle()}
|
||||
onDragEnter={this.handleDragEnter}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDrop={this.handleDrop}
|
||||
>
|
||||
{/* frozen */}
|
||||
<div
|
||||
className="frozen-columns d-flex"
|
||||
style={this.getFrozenColumnsStyle()}
|
||||
ref={ref => this.frozenColumns = ref}
|
||||
>
|
||||
{showSequenceColumn &&
|
||||
<ActionsCell
|
||||
isLocked={isLocked}
|
||||
isSelected={isSelected}
|
||||
recordId={record._id}
|
||||
index={index}
|
||||
onSelectRecord={this.onSelectRecord}
|
||||
isLastFrozenCell={!lastFrozenColumnKey}
|
||||
height={cellHeight}
|
||||
/>
|
||||
}
|
||||
{frozenCells}
|
||||
</div>
|
||||
{/* scroll */}
|
||||
{columnCells}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Record.propTypes = {
|
||||
hasSelectedCell: PropTypes.bool,
|
||||
isGroupView: PropTypes.bool,
|
||||
isSelected: PropTypes.bool,
|
||||
groupRecordIndex: PropTypes.number,
|
||||
index: PropTypes.number.isRequired,
|
||||
isLastRecord: PropTypes.bool,
|
||||
lastFrozenColumnKey: PropTypes.string,
|
||||
cellMetaData: PropTypes.object,
|
||||
selectedPosition: PropTypes.object,
|
||||
record: PropTypes.object.isRequired,
|
||||
columns: PropTypes.array.isRequired,
|
||||
showSequenceColumn: PropTypes.bool,
|
||||
sequenceColumnWidth: PropTypes.number,
|
||||
colOverScanStartIdx: PropTypes.number,
|
||||
colOverScanEndIdx: PropTypes.number,
|
||||
scrollLeft: PropTypes.number,
|
||||
top: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
selectNoneCells: PropTypes.func,
|
||||
onSelectRecord: PropTypes.func,
|
||||
checkCanModifyRecord: PropTypes.func,
|
||||
checkCellValueChanged: PropTypes.func,
|
||||
reloadRecords: PropTypes.func,
|
||||
searchResult: PropTypes.object,
|
||||
columnColor: PropTypes.object,
|
||||
};
|
||||
|
||||
export default Record;
|
53
frontend/src/components/sf-table/utils/cell-comparer.js
Normal file
53
frontend/src/components/sf-table/utils/cell-comparer.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import ObjectUtils, { isEmptyObject } from '../../../utils/object';
|
||||
import { getCellValueByColumn } from './cell';
|
||||
|
||||
export const checkCellValueChanged = (oldVal, newVal) => {
|
||||
if (oldVal === newVal) return false;
|
||||
if (oldVal === undefined || oldVal === null || oldVal === '') {
|
||||
if (newVal === undefined || newVal === null || newVal === '') return false;
|
||||
if (typeof newVal === 'object' && isEmptyObject(newVal)) return false;
|
||||
if (Array.isArray(newVal)) return newVal.length !== 0;
|
||||
if (typeof newVal === 'boolean') return newVal !== false;
|
||||
}
|
||||
if (Array.isArray(oldVal) && Array.isArray(newVal)) {
|
||||
// [{}].toString(): [object Object]
|
||||
return JSON.stringify(oldVal) !== JSON.stringify(newVal);
|
||||
}
|
||||
if (typeof oldVal === 'object' && typeof newVal === 'object' && newVal !== null) {
|
||||
return !ObjectUtils.isSameObject(oldVal, newVal);
|
||||
}
|
||||
return oldVal !== newVal;
|
||||
};
|
||||
|
||||
export const cellCompare = (props, nextProps) => {
|
||||
const {
|
||||
record: oldRecord, column, isCellSelected, isLastCell, highlightClassName, height, bgColor,
|
||||
} = props;
|
||||
const {
|
||||
record: newRecord, highlightClassName: newHighlightClassName, height: newHeight, column: newColumn, bgColor: newBgColor,
|
||||
} = nextProps;
|
||||
|
||||
// the modification of column is not currently supported, only the modification of cell data is considered
|
||||
const oldValue = getCellValueByColumn(oldRecord, column);
|
||||
const newValue = getCellValueByColumn(newRecord, column);
|
||||
let isCustomCellValueChanged = false;
|
||||
if (props.checkCellValueChanged) {
|
||||
isCustomCellValueChanged = props.checkCellValueChanged(column, oldRecord, newRecord);
|
||||
}
|
||||
return (
|
||||
isCustomCellValueChanged ||
|
||||
checkCellValueChanged(oldValue, newValue) ||
|
||||
oldRecord._last_modifier !== newRecord._last_modifier ||
|
||||
isCellSelected !== nextProps.isCellSelected ||
|
||||
isLastCell !== nextProps.isLastCell ||
|
||||
highlightClassName !== newHighlightClassName ||
|
||||
height !== newHeight ||
|
||||
column.name !== newColumn.name ||
|
||||
column.left !== newColumn.left ||
|
||||
column.width !== newColumn.width ||
|
||||
!ObjectUtils.isSameObject(column.data, newColumn.data) ||
|
||||
bgColor !== newBgColor ||
|
||||
props.groupRecordIndex !== nextProps.groupRecordIndex ||
|
||||
props.recordIndex !== nextProps.recordIndex
|
||||
);
|
||||
};
|
12
frontend/src/components/sf-table/utils/cell.js
Normal file
12
frontend/src/components/sf-table/utils/cell.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { checkIsPrivateColumn } from './column';
|
||||
|
||||
/*
|
||||
* @param {object} record eg: { [column_key]: value, [column_name]: value }
|
||||
* @param {object} column
|
||||
* @return {any} value
|
||||
*/
|
||||
export const getCellValueByColumn = (record, column) => {
|
||||
if (!record || !column) return null;
|
||||
const { key, name } = column;
|
||||
return checkIsPrivateColumn(column) ? record[key] : record[name];
|
||||
};
|
123
frontend/src/components/sf-table/utils/column.js
Normal file
123
frontend/src/components/sf-table/utils/column.js
Normal file
@@ -0,0 +1,123 @@
|
||||
import { shallowCloneObject } from '../../../utils/object';
|
||||
|
||||
export const checkIsNameColumn = (column) => {
|
||||
if (!column) return false;
|
||||
return !!column.is_name_column;
|
||||
};
|
||||
|
||||
export const checkIsColumnFrozen = (column) => {
|
||||
if (!column) return false;
|
||||
return !!column.frozen;
|
||||
};
|
||||
|
||||
export const checkEditableViaClickCell = (column) => {
|
||||
if (!column) return false;
|
||||
return !!column.editable_via_click_cell;
|
||||
};
|
||||
|
||||
export const checkIsColumnSupportDirectEdit = (column) => {
|
||||
if (!column) return false;
|
||||
return !!column.is_support_direct_edit;
|
||||
};
|
||||
|
||||
export const checkIsColumnSupportPreview = (column) => {
|
||||
if (!column) return false;
|
||||
return !!column.is_support_preview;
|
||||
};
|
||||
|
||||
export const checkIsColumnEditable = (column) => {
|
||||
if (!column) return false;
|
||||
return !!column.editable;
|
||||
};
|
||||
|
||||
export const checkIsPopupColumnEditor = (column) => {
|
||||
if (!column) return false;
|
||||
return !!column.is_popup_editor;
|
||||
};
|
||||
|
||||
export const checkIsPrivateColumn = (column) => {
|
||||
if (!column) return false;
|
||||
return !!column.is_private;
|
||||
};
|
||||
|
||||
export const getColumnOriginName = (column) => {
|
||||
if (checkIsPrivateColumn(column)) return column.key;
|
||||
return column.name;
|
||||
};
|
||||
|
||||
export const getColumnByKey = (columns, columnKey) => {
|
||||
if (!Array.isArray(columns) || !columnKey) return null;
|
||||
return columns.find((column) => column.key === columnKey);
|
||||
};
|
||||
|
||||
export const getColumnIndexByKey = (columnKey, columns) => {
|
||||
let index = 0;
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
if (columnKey === columns[i].key) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return index;
|
||||
};
|
||||
|
||||
export function getColumnByIndex(index, columns) {
|
||||
if (Array.isArray(columns)) {
|
||||
return columns[index];
|
||||
}
|
||||
if (typeof Immutable !== 'undefined') {
|
||||
return columns.get(index);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const getFrozenColumns = (columns) => {
|
||||
return columns.filter(column => checkIsColumnFrozen(column));
|
||||
};
|
||||
|
||||
export const recalculate = (columns, allColumns, sequenceColumnWidth = 0) => {
|
||||
const displayColumns = columns;
|
||||
const displayAllColumns = allColumns;
|
||||
const totalWidth = displayColumns.reduce((total, column) => {
|
||||
const width = column.width;
|
||||
total += width;
|
||||
return total;
|
||||
}, 0);
|
||||
let left = sequenceColumnWidth;
|
||||
const frozenColumns = displayColumns.filter(c => checkIsColumnFrozen(c));
|
||||
const frozenColumnsWidth = frozenColumns.reduce((w, column) => {
|
||||
const width = column.width;
|
||||
return w + width;
|
||||
}, 0);
|
||||
const lastFrozenColumnKey = frozenColumnsWidth > 0 ? frozenColumns[frozenColumns.length - 1].key : null;
|
||||
const newColumns = displayColumns.map((column, index) => {
|
||||
const width = column.width;
|
||||
column.idx = index; // set column idx
|
||||
column.left = left; // set column offset
|
||||
column.width = width;
|
||||
left += width;
|
||||
return column;
|
||||
});
|
||||
|
||||
return {
|
||||
totalWidth,
|
||||
lastFrozenColumnKey,
|
||||
frozenColumnsWidth,
|
||||
columns: newColumns,
|
||||
allColumns: displayAllColumns,
|
||||
};
|
||||
};
|
||||
|
||||
export const recalculateColumnMetricsByResizeColumn = (columnMetrics, sequenceColumnWidth, columnKey, width) => {
|
||||
let newColumnMetrics = shallowCloneObject(columnMetrics);
|
||||
let updatedColumns = columnMetrics.columns.map((column) => shallowCloneObject(column));
|
||||
const columnIndex = updatedColumns.findIndex((column) => column.key === columnKey);
|
||||
let updatedColumn = updatedColumns[columnIndex];
|
||||
updatedColumn.width = width;
|
||||
newColumnMetrics.columns = updatedColumns;
|
||||
|
||||
const columnAllIndex = columnMetrics.allColumns.findIndex((column) => column.key === columnKey);
|
||||
newColumnMetrics.allColumns[columnAllIndex] = { ...columnMetrics.columns[columnIndex], width };
|
||||
|
||||
return recalculate(newColumnMetrics.columns, newColumnMetrics.allColumns, sequenceColumnWidth);
|
||||
};
|
107
frontend/src/components/sf-table/utils/get-event-transfer.js
Normal file
107
frontend/src/components/sf-table/utils/get-event-transfer.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import TRANSFER_TYPES from '../constants/transfer-types';
|
||||
|
||||
const { FRAGMENT, HTML, TEXT } = TRANSFER_TYPES;
|
||||
|
||||
function getEventTransfer(event) {
|
||||
const transfer = event.dataTransfer || event.clipboardData;
|
||||
let dtableFragment = getType(transfer, FRAGMENT);
|
||||
let html = getType(transfer, HTML);
|
||||
let text = getType(transfer, TEXT);
|
||||
let files = getFiles(transfer);
|
||||
|
||||
// paste sf-metadata
|
||||
if (dtableFragment) {
|
||||
return { [TRANSFER_TYPES.METADATA_FRAGMENT]: JSON.parse(dtableFragment), type: TRANSFER_TYPES.METADATA_FRAGMENT };
|
||||
}
|
||||
|
||||
// paste html
|
||||
if (html) {
|
||||
let copiedTableNode = (new DOMParser()).parseFromString(html, HTML).querySelector('table');
|
||||
if (copiedTableNode) {
|
||||
return { [TRANSFER_TYPES.METADATA_FRAGMENT]: html2TableFragment(copiedTableNode), html, text, type: 'html' };
|
||||
}
|
||||
return { [TRANSFER_TYPES.METADATA_FRAGMENT]: text2TableFragment(text), html, text, type: 'html' };
|
||||
}
|
||||
|
||||
// paste local picture or other files here
|
||||
if (files && files.length) {
|
||||
return { [TRANSFER_TYPES.METADATA_FRAGMENT]: text2TableFragment(text), 'files': files, type: 'files' };
|
||||
}
|
||||
|
||||
// paste text
|
||||
if (text) {
|
||||
return { [TRANSFER_TYPES.METADATA_FRAGMENT]: text2TableFragment(text), text, type: 'text' };
|
||||
}
|
||||
}
|
||||
|
||||
function getType(transfer, type) {
|
||||
if (!transfer.types || !transfer.types.length) {
|
||||
// COMPAT: In IE 11, there is no `types` field but `getData('Text')`
|
||||
// is supported`. (2017/06/23)
|
||||
return type === TEXT ? transfer.getData('Text') || null : null;
|
||||
}
|
||||
|
||||
return transfer.getData(type);
|
||||
}
|
||||
|
||||
function text2TableFragment(data) {
|
||||
let formattedData = data ? data.replace(/\r/g, '') : '';
|
||||
let dataSplitted = formattedData.split('\n');
|
||||
let rowSplitted = dataSplitted[0].split('\t');
|
||||
let copiedColumns = rowSplitted.map((value, j) => ({ key: `col${j}`, type: 'text' }));
|
||||
let copiedRecords = [];
|
||||
dataSplitted.forEach((row) => {
|
||||
let obj = {};
|
||||
if (row) {
|
||||
row = row.split('\t');
|
||||
row.forEach((col, j) => {
|
||||
obj[`col${j}`] = col;
|
||||
});
|
||||
}
|
||||
copiedRecords.push(obj);
|
||||
});
|
||||
|
||||
return { copiedRecords, copiedColumns };
|
||||
}
|
||||
|
||||
function html2TableFragment(tableNode) {
|
||||
let trs = tableNode.querySelectorAll('tr');
|
||||
let tds = trs[0].querySelectorAll('td');
|
||||
let copiedColumns = [];
|
||||
let copiedRecords = [];
|
||||
tds.forEach((td, i) => {
|
||||
copiedColumns.push({ key: `col${i}`, type: 'text' });
|
||||
});
|
||||
trs.forEach((tr) => {
|
||||
let row = {};
|
||||
let cells = tr.querySelectorAll('td');
|
||||
cells.forEach((cell, i) => {
|
||||
row[`col${i}`] = cell.innerText;
|
||||
});
|
||||
copiedRecords.push(row);
|
||||
});
|
||||
return { copiedRecords, copiedColumns };
|
||||
}
|
||||
|
||||
function getFiles(transfer) {
|
||||
let files;
|
||||
try {
|
||||
// Get and normalize files if they exist.
|
||||
if (transfer.items && transfer.items.length) {
|
||||
files = Array.from(transfer.items)
|
||||
.map(item => (item.kind === 'file' ? item.getAsFile() : null))
|
||||
.filter(exists => exists);
|
||||
} else if (transfer.files && transfer.files.length) {
|
||||
files = Array.from(transfer.files);
|
||||
}
|
||||
} catch (err) {
|
||||
if (transfer.files && transfer.files.length) {
|
||||
files = Array.from(transfer.files);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
export { text2TableFragment };
|
||||
|
||||
export default getEventTransfer;
|
53
frontend/src/components/sf-table/utils/grid-utils.js
Normal file
53
frontend/src/components/sf-table/utils/grid-utils.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import TRANSFER_TYPES from '../constants/transfer-types';
|
||||
import { getColumnByIndex } from './column';
|
||||
|
||||
|
||||
class GridUtils {
|
||||
|
||||
constructor(renderRecordsIds, { recordGetterById, recordGetterByIndex }) {
|
||||
this.renderRecordsIds = renderRecordsIds;
|
||||
this.api = {
|
||||
recordGetterById,
|
||||
recordGetterByIndex,
|
||||
};
|
||||
}
|
||||
|
||||
getCopiedContent({ type, copied, isGroupView, columns }) {
|
||||
// copy from internal grid
|
||||
if (type === TRANSFER_TYPES.METADATA_FRAGMENT) {
|
||||
const { selectedRecordIds, copiedRange } = copied;
|
||||
|
||||
// copy from selected rows
|
||||
if (Array.isArray(selectedRecordIds) && selectedRecordIds.length > 0) {
|
||||
return {
|
||||
copiedRecords: selectedRecordIds.map(recordId => this.api.recordGetterById(recordId)),
|
||||
copiedColumns: [...columns],
|
||||
};
|
||||
}
|
||||
|
||||
// copy from selected range
|
||||
let copiedRecords = [];
|
||||
let copiedColumns = [];
|
||||
const { topLeft, bottomRight } = copiedRange;
|
||||
const { rowIdx: minRecordIndex, idx: minColumnIndex, groupRecordIndex: minGroupRecordIndex } = topLeft;
|
||||
const { rowIdx: maxRecordIndex, idx: maxColumnIndex } = bottomRight;
|
||||
let currentGroupIndex = minGroupRecordIndex;
|
||||
for (let i = minRecordIndex; i <= maxRecordIndex; i++) {
|
||||
copiedRecords.push(this.api.recordGetterByIndex({ isGroupView, groupRecordIndex: currentGroupIndex, recordIndex: i }));
|
||||
if (isGroupView) {
|
||||
currentGroupIndex++;
|
||||
}
|
||||
}
|
||||
for (let i = minColumnIndex; i <= maxColumnIndex; i++) {
|
||||
copiedColumns.push(getColumnByIndex(i, columns));
|
||||
}
|
||||
return { copiedRecords, copiedColumns };
|
||||
}
|
||||
|
||||
// copy from other external apps as default
|
||||
const { copiedRecords, copiedColumns } = copied;
|
||||
return { copiedRecords, copiedColumns };
|
||||
}
|
||||
}
|
||||
|
||||
export default GridUtils;
|
9
frontend/src/components/sf-table/utils/grid.js
Normal file
9
frontend/src/components/sf-table/utils/grid.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { OVER_SCAN_COLUMNS } from '../constants/grid';
|
||||
|
||||
export const getColOverScanStartIdx = (colVisibleStartIdx) => {
|
||||
return Math.max(0, Math.floor(colVisibleStartIdx / 10) * 10 - OVER_SCAN_COLUMNS);
|
||||
};
|
||||
|
||||
export const getColOverScanEndIdx = (colVisibleEndIdx, totalNumberColumns) => {
|
||||
return Math.min(Math.ceil(colVisibleEndIdx / 10) * 10 + OVER_SCAN_COLUMNS, totalNumberColumns);
|
||||
};
|
173
frontend/src/components/sf-table/utils/group-metrics.js
Normal file
173
frontend/src/components/sf-table/utils/group-metrics.js
Normal file
@@ -0,0 +1,173 @@
|
||||
import { INSERT_ROW_HEIGHT } from '../constants/grid';
|
||||
import { GROUP_HEADER_HEIGHT, GROUP_ROW_TYPE, GROUP_VIEW_OFFSET } from '../constants/group';
|
||||
import { getColumnByKey } from './column';
|
||||
|
||||
export const createGroupMetrics = (groups, groupbys, pathFoldedGroupMap, columns, rowHeight, includeInsertRow) => {
|
||||
let groupbyColumnsMap = {};
|
||||
groupbys.forEach(groupby => {
|
||||
const columnKey = groupby.column_key;
|
||||
const column = getColumnByKey(columns, columnKey);
|
||||
groupbyColumnsMap[columnKey] = column;
|
||||
});
|
||||
const maxLevel = groupbys.length;
|
||||
const groupRows = getGroupsRows(
|
||||
groups, groupbyColumnsMap, pathFoldedGroupMap, includeInsertRow, rowHeight, maxLevel,
|
||||
{ parentGroupPath: [], currentLevel: maxLevel, isParentGroupVisible: true }
|
||||
);
|
||||
const { computedGroupRows, groupRowsHeight, idGroupRowMap } = setupGroupsRows(groupRows, maxLevel);
|
||||
return {
|
||||
groupRows: computedGroupRows,
|
||||
idGroupRowMap,
|
||||
groupRowsHeight,
|
||||
maxLevel,
|
||||
};
|
||||
};
|
||||
|
||||
export const getGroupsRows = (
|
||||
groups, groupbyColumnsMap, pathFoldedGroupMap, includeInsertRow, rowHeight, maxLevel, {
|
||||
parentGroupPath, parentGroupKey, currentLevel, isParentGroupVisible,
|
||||
}
|
||||
) => {
|
||||
let groupRows = [];
|
||||
groups.forEach((group, groupIndex) => {
|
||||
let groupPath = [];
|
||||
if (parentGroupPath.length > 0) {
|
||||
groupPath.push(...parentGroupPath);
|
||||
}
|
||||
groupPath.push(groupIndex);
|
||||
const { cell_value, subgroups, row_ids, column_key, summaries, original_cell_value } = group;
|
||||
const groupPathString = groupPath.join('-');
|
||||
const isExpanded = isExpandedGroup(groupPathString, pathFoldedGroupMap);
|
||||
const left = (maxLevel - currentLevel + 1) * GROUP_VIEW_OFFSET;
|
||||
const groupKey = `${parentGroupKey ? parentGroupKey : column_key}_${cell_value}`;
|
||||
let groupContainer = {
|
||||
type: GROUP_ROW_TYPE.GROUP_CONTAINER,
|
||||
level: currentLevel,
|
||||
left,
|
||||
key: groupKey,
|
||||
cell_value,
|
||||
column_key,
|
||||
isExpanded,
|
||||
summaries,
|
||||
groupPath,
|
||||
groupPathString,
|
||||
column: groupbyColumnsMap[column_key],
|
||||
visible: isParentGroupVisible,
|
||||
original_cell_value
|
||||
};
|
||||
if (Array.isArray(subgroups) && subgroups.length > 0) {
|
||||
const flattenSubgroups = getGroupsRows(
|
||||
subgroups, groupbyColumnsMap, pathFoldedGroupMap, includeInsertRow, rowHeight, maxLevel,
|
||||
{ parentGroupPath: groupPath, parentGroupKey: groupKey, currentLevel: currentLevel - 1, isParentGroupVisible: isParentGroupVisible && isExpanded }
|
||||
);
|
||||
let groupCount = 0;
|
||||
let subgroupsHeight = 0;
|
||||
let first_row_id;
|
||||
flattenSubgroups.forEach((subgroupContainer) => {
|
||||
if (subgroupContainer.type === GROUP_ROW_TYPE.GROUP_CONTAINER && subgroupContainer.level + 1 === currentLevel) {
|
||||
groupCount += subgroupContainer.count || 0;
|
||||
subgroupsHeight += (subgroupContainer.height || 0) + GROUP_VIEW_OFFSET;
|
||||
if (!first_row_id) {
|
||||
first_row_id = subgroupContainer.first_row_id;
|
||||
}
|
||||
}
|
||||
});
|
||||
groupContainer.first_row_id = first_row_id;
|
||||
groupContainer.count = groupCount;
|
||||
groupContainer.height = (isExpanded ? subgroupsHeight : 0) + GROUP_HEADER_HEIGHT;
|
||||
groupRows.push(groupContainer);
|
||||
groupRows.push(...flattenSubgroups);
|
||||
} else if (Array.isArray(row_ids) && row_ids.length > 0) {
|
||||
const rowsLength = row_ids.length;
|
||||
const lastRowIndex = rowsLength - 1;
|
||||
const isRowVisible = isParentGroupVisible && isExpanded;
|
||||
const isBtnInsertRowVisible = isRowVisible && includeInsertRow;
|
||||
const rowsHeight = isRowVisible ? rowsLength * rowHeight + 1 : 0;
|
||||
const btnInsertRowHeight = isBtnInsertRowVisible ? INSERT_ROW_HEIGHT : 0;
|
||||
let rows = row_ids.map((rowId, index) => {
|
||||
return {
|
||||
type: GROUP_ROW_TYPE.ROW,
|
||||
key: `row-${rowId}`,
|
||||
rowIdx: index,
|
||||
isLastRow: index === lastRowIndex,
|
||||
visible: isRowVisible,
|
||||
height: index === lastRowIndex ? rowHeight + 1 : rowHeight,
|
||||
level: currentLevel,
|
||||
rowsLength,
|
||||
left,
|
||||
rowId,
|
||||
groupPath,
|
||||
groupPathString,
|
||||
};
|
||||
});
|
||||
groupContainer.first_row_id = rows[0].rowId;
|
||||
groupContainer.count = rowsLength;
|
||||
groupContainer.height = rowsHeight + btnInsertRowHeight + GROUP_HEADER_HEIGHT;
|
||||
groupRows.push(groupContainer);
|
||||
groupRows.push(...rows);
|
||||
}
|
||||
});
|
||||
return groupRows;
|
||||
};
|
||||
|
||||
export const setupGroupsRows = (groupRows, maxLevel) => {
|
||||
let groupRowsHeight = GROUP_VIEW_OFFSET;
|
||||
let top = GROUP_VIEW_OFFSET;
|
||||
let idGroupRowMap = {};
|
||||
let pervVisibleGroupLevel;
|
||||
const computedGroupRows = groupRows.map((flattenGroup, index) => {
|
||||
const { type, level, height, visible } = flattenGroup;
|
||||
let newGroupRow = {
|
||||
...flattenGroup,
|
||||
top,
|
||||
groupRecordIndex: index,
|
||||
};
|
||||
if (type === GROUP_ROW_TYPE.GROUP_CONTAINER) {
|
||||
if (visible) {
|
||||
if (level === maxLevel) {
|
||||
groupRowsHeight += height + GROUP_VIEW_OFFSET;
|
||||
}
|
||||
top += GROUP_HEADER_HEIGHT;
|
||||
pervVisibleGroupLevel = level;
|
||||
}
|
||||
} else if (type === GROUP_ROW_TYPE.ROW) {
|
||||
const { rowId } = flattenGroup;
|
||||
idGroupRowMap[rowId] = newGroupRow;
|
||||
if (visible) {
|
||||
top += height;
|
||||
}
|
||||
} else if (type === GROUP_ROW_TYPE.BTN_INSERT_ROW) {
|
||||
if (visible) {
|
||||
top += height;
|
||||
}
|
||||
}
|
||||
const nextFlattenGroup = groupRows[index + 1];
|
||||
if (nextFlattenGroup && nextFlattenGroup.visible && nextFlattenGroup.type === GROUP_ROW_TYPE.GROUP_CONTAINER) {
|
||||
const { groupPath: nextGroupPath, level: nextGroupLevel } = nextFlattenGroup;
|
||||
if (nextGroupPath[nextGroupPath.length - 1] > 0) {
|
||||
top += GROUP_VIEW_OFFSET;
|
||||
}
|
||||
if (nextGroupLevel > pervVisibleGroupLevel) {
|
||||
top += (nextGroupLevel - pervVisibleGroupLevel) * GROUP_VIEW_OFFSET;
|
||||
}
|
||||
}
|
||||
return newGroupRow;
|
||||
});
|
||||
return { computedGroupRows, groupRowsHeight, idGroupRowMap };
|
||||
};
|
||||
|
||||
export const isExpandedGroup = (groupPathString, pathFoldedGroupMap) => {
|
||||
return !pathFoldedGroupMap || !pathFoldedGroupMap[groupPathString];
|
||||
};
|
||||
|
||||
export const isNestedGroupRow = (currentGroupRow, targetGroupRow) => {
|
||||
const { groupPath: currentGroupPath, groupPathString: currentGroupPathString, level: currentGroupLevel, type: currentGroupRowType } = currentGroupRow;
|
||||
const { groupPath: targetGroupPath, groupPathString: targetGroupPathString, level: targetGroupLevel } = targetGroupRow;
|
||||
return (currentGroupPathString === targetGroupPathString && currentGroupRowType !== GROUP_ROW_TYPE.GROUP_CONTAINER) ||
|
||||
(currentGroupLevel < targetGroupLevel && currentGroupPath[0] === targetGroupPath[0]);
|
||||
};
|
||||
|
||||
export const getGroupRecordByIndex = (index, groupMetrics) => {
|
||||
const groupRows = groupMetrics.groupRows || [];
|
||||
return groupRows[index] || {};
|
||||
};
|
35
frontend/src/components/sf-table/utils/group.js
Normal file
35
frontend/src/components/sf-table/utils/group.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Get group by paths
|
||||
* @param {array} paths e.g. [ 0, 1, 2 ]
|
||||
* @param {array} groups grouped rows
|
||||
* @returns group, object
|
||||
*/
|
||||
export const getGroupByPath = (paths, groups) => {
|
||||
if (!Array.isArray(paths) || !Array.isArray(groups)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const level0GroupIndex = paths[0];
|
||||
if (level0GroupIndex < 0 || level0GroupIndex >= groups.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let level = 1;
|
||||
let foundGroup = groups[level0GroupIndex];
|
||||
while (level < paths.length) {
|
||||
if (!foundGroup) {
|
||||
break;
|
||||
}
|
||||
const subGroups = foundGroup.subgroups;
|
||||
const currentLevelGroupIndex = paths[level];
|
||||
if (
|
||||
!Array.isArray(subGroups)
|
||||
|| (currentLevelGroupIndex < 0 || currentLevelGroupIndex >= subGroups.length)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
foundGroup = subGroups[currentLevelGroupIndex];
|
||||
level += 1;
|
||||
}
|
||||
return foundGroup;
|
||||
};
|
38
frontend/src/components/sf-table/utils/index.js
Normal file
38
frontend/src/components/sf-table/utils/index.js
Normal file
@@ -0,0 +1,38 @@
|
||||
export const addClassName = (originClassName, targetClassName) => {
|
||||
const originClassNames = originClassName.split(' ');
|
||||
if (originClassNames.indexOf(targetClassName) > -1) return originClassName;
|
||||
return originClassName + ' ' + targetClassName;
|
||||
};
|
||||
|
||||
export const removeClassName = (originClassName, targetClassName) => {
|
||||
let originClassNames = originClassName.split(' ');
|
||||
const targetClassNameIndex = originClassNames.indexOf(targetClassName);
|
||||
if (targetClassNameIndex < 0) return originClassName;
|
||||
originClassNames.splice(targetClassNameIndex, 1);
|
||||
return originClassNames.join(' ');
|
||||
};
|
||||
|
||||
export const getEventClassName = (e) => {
|
||||
// svg mouseEvent event.target.className is an object
|
||||
if (!e || !e.target) return '';
|
||||
return e.target.getAttribute('class') || '';
|
||||
};
|
||||
|
||||
/* is weiXin built-in browser */
|
||||
export const isWeiXinBuiltInBrowser = () => {
|
||||
const agent = navigator.userAgent.toLowerCase();
|
||||
if (agent.match(/MicroMessenger/i) === 'micromessenger' ||
|
||||
(typeof window.WeixinJSBridge !== 'undefined')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isWindowsBrowser = () => {
|
||||
return /windows|win32/i.test(navigator.userAgent);
|
||||
};
|
||||
|
||||
export const isWebkitBrowser = () => {
|
||||
let agent = navigator.userAgent.toLowerCase();
|
||||
return agent.includes('webkit');
|
||||
};
|
56
frontend/src/components/sf-table/utils/record-metrics.js
Normal file
56
frontend/src/components/sf-table/utils/record-metrics.js
Normal file
@@ -0,0 +1,56 @@
|
||||
function selectRecord(recordId, recordMetrics) {
|
||||
if (isRecordSelected(recordId, recordMetrics)) {
|
||||
return;
|
||||
}
|
||||
recordMetrics.idSelectedRecordMap[recordId] = true;
|
||||
}
|
||||
|
||||
function selectRecordsById(recordIds, recordMetrics) {
|
||||
recordIds.forEach(recordId => {
|
||||
selectRecord(recordId, recordMetrics);
|
||||
});
|
||||
}
|
||||
|
||||
function deselectRecord(recordId, recordMetrics) {
|
||||
if (!isRecordSelected(recordId, recordMetrics)) {
|
||||
return;
|
||||
}
|
||||
delete recordMetrics.idSelectedRecordMap[recordId];
|
||||
}
|
||||
|
||||
function deselectAllRecords(recordMetrics) {
|
||||
recordMetrics.idSelectedRecordMap = {};
|
||||
}
|
||||
|
||||
function isRecordSelected(recordId, recordMetrics) {
|
||||
return recordMetrics.idSelectedRecordMap[recordId];
|
||||
}
|
||||
|
||||
function getSelectedIds(recordMetrics) {
|
||||
return Object.keys(recordMetrics.idSelectedRecordMap);
|
||||
}
|
||||
|
||||
function hasSelectedRecords(recordMetrics) {
|
||||
return getSelectedIds(recordMetrics).length > 0;
|
||||
}
|
||||
|
||||
function isSelectedAll(recordIds, recordMetrics) {
|
||||
const selectedRecordsLen = getSelectedIds(recordMetrics).length;
|
||||
if (selectedRecordsLen === 0) {
|
||||
return false;
|
||||
}
|
||||
return recordIds.every(recordId => isRecordSelected(recordId, recordMetrics));
|
||||
}
|
||||
|
||||
const recordMetrics = {
|
||||
selectRecord,
|
||||
selectRecordsById,
|
||||
deselectRecord,
|
||||
deselectAllRecords,
|
||||
isRecordSelected,
|
||||
getSelectedIds,
|
||||
hasSelectedRecords,
|
||||
isSelectedAll,
|
||||
};
|
||||
|
||||
export default recordMetrics;
|
53
frontend/src/components/sf-table/utils/records-body-utils.js
Normal file
53
frontend/src/components/sf-table/utils/records-body-utils.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { isMobile } from '../../../utils/utils';
|
||||
import { checkIsColumnFrozen } from './column';
|
||||
|
||||
export const getColumnScrollPosition = (columns, idx, tableContentWidth) => {
|
||||
let left = 0;
|
||||
let frozen = 0;
|
||||
const selectedColumn = getColumn(columns, idx);
|
||||
if (!selectedColumn) return null;
|
||||
|
||||
for (let i = 0; i < idx; i++) {
|
||||
const column = getColumn(columns, i);
|
||||
if (column) {
|
||||
if (column.width) {
|
||||
left += column.width;
|
||||
}
|
||||
if (checkIsColumnFrozen(column)) {
|
||||
frozen += column.width;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isMobile ? left - (tableContentWidth - selectedColumn.width) / 2 : left - frozen;
|
||||
};
|
||||
|
||||
export const getColumn = (columns, idx) => {
|
||||
if (Array.isArray(columns)) {
|
||||
return columns[idx];
|
||||
} else if (typeof Immutable !== 'undefined') {
|
||||
return columns.get(idx);
|
||||
}
|
||||
};
|
||||
|
||||
export const getColVisibleStartIdx = (columns, scrollLeft) => {
|
||||
let remainingScroll = scrollLeft;
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
let { width } = columns[i];
|
||||
remainingScroll -= width;
|
||||
if (remainingScroll < 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getColVisibleEndIdx = (columns, recordBodyWidth, scrollLeft) => {
|
||||
let usefulWidth = recordBodyWidth + scrollLeft;
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
let { width } = columns[i];
|
||||
usefulWidth -= width;
|
||||
if (usefulWidth < 0) {
|
||||
return i - 1 - 1;
|
||||
}
|
||||
}
|
||||
return columns.length - 1;
|
||||
};
|
196
frontend/src/components/sf-table/utils/selected-cell-utils.js
Normal file
196
frontend/src/components/sf-table/utils/selected-cell-utils.js
Normal file
@@ -0,0 +1,196 @@
|
||||
// import { Utils } from '../../../utils/utils';
|
||||
import { CELL_MASK as Z_INDEX_CELL_MASK, FROZEN_CELL_MASK as Z_INDEX_FROZEN_CELL_MASK } from '../constants/z-index';
|
||||
import { getCellValueByColumn } from './cell';
|
||||
import { checkIsColumnEditable, checkIsColumnSupportPreview, getColumnByIndex } from './column';
|
||||
import { getGroupByPath } from './group';
|
||||
import { getGroupRecordByIndex } from './group-metrics';
|
||||
|
||||
const SELECT_DIRECTION = {
|
||||
UP: 'upwards',
|
||||
DOWN: 'downwards',
|
||||
};
|
||||
|
||||
export const getRowTop = (rowIdx, rowHeight) => rowIdx * rowHeight;
|
||||
|
||||
export const getSelectedRow = ({ selectedPosition, isGroupView, recordGetterByIndex }) => {
|
||||
const { groupRecordIndex, rowIdx } = selectedPosition;
|
||||
return recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex: rowIdx });
|
||||
};
|
||||
|
||||
export const getSelectedColumn = ({ selectedPosition, columns }) => {
|
||||
const { idx } = selectedPosition;
|
||||
return getColumnByIndex(idx, columns);
|
||||
};
|
||||
|
||||
export const getSelectedCellValue = ({ selectedPosition, columns, isGroupView, recordGetterByIndex }) => {
|
||||
const column = getSelectedColumn({ selectedPosition, columns });
|
||||
const record = getSelectedRow({ selectedPosition, isGroupView, recordGetterByIndex });
|
||||
return getCellValueByColumn(record, column);
|
||||
};
|
||||
|
||||
export const checkIsCellSupportOpenEditor = (cell, column, isGroupView, recordGetterByIndex, checkCanModifyRecord) => {
|
||||
const { groupRecordIndex, rowIdx } = cell;
|
||||
if (!column) return false;
|
||||
|
||||
// open the editor to preview cell value
|
||||
if (checkIsColumnSupportPreview(column)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!checkIsColumnEditable(column)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const record = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex: rowIdx });
|
||||
if (!record || !checkCanModifyRecord) return false;
|
||||
return !!checkCanModifyRecord(record);
|
||||
};
|
||||
|
||||
export const checkIsSelectedCellEditable = ({ enableCellSelect, selectedPosition, columns, isGroupView, recordGetterByIndex, checkCanModifyRecord }) => {
|
||||
const column = getSelectedColumn({ selectedPosition, columns });
|
||||
if (!checkIsColumnEditable(column)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const row = getSelectedRow({ selectedPosition, isGroupView, recordGetterByIndex });
|
||||
if (!row || !checkCanModifyRecord) {
|
||||
return false;
|
||||
}
|
||||
return checkCanModifyRecord(row);
|
||||
};
|
||||
|
||||
export function selectedRangeIsSingleCell(selectedRange) {
|
||||
const { topLeft, bottomRight } = selectedRange;
|
||||
if (
|
||||
topLeft.idx !== bottomRight.idx ||
|
||||
topLeft.rowIdx !== bottomRight.rowIdx
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export const getSelectedDimensions = ({
|
||||
selectedPosition, columns, rowHeight, scrollLeft, isGroupView, groupOffsetLeft,
|
||||
getRecordTopFromRecordsBody,
|
||||
}) => {
|
||||
const { idx, rowIdx, groupRecordIndex } = selectedPosition;
|
||||
const defaultDimensions = { width: 0, left: 0, top: 0, height: rowHeight, zIndex: 1 };
|
||||
if (idx >= 0) {
|
||||
const column = columns && columns[idx];
|
||||
if (!column) {
|
||||
return defaultDimensions;
|
||||
}
|
||||
const { frozen, width } = column;
|
||||
let left = frozen ? scrollLeft + column.left : column.left;
|
||||
let top;
|
||||
if (isGroupView) {
|
||||
left += groupOffsetLeft;
|
||||
// group view uses border-top, No group view uses border-bottom (for group animation) so selected top should be increased 1
|
||||
top = getRecordTopFromRecordsBody(groupRecordIndex) + 1;
|
||||
} else {
|
||||
top = getRecordTopFromRecordsBody(rowIdx);
|
||||
}
|
||||
const zIndex = frozen ? Z_INDEX_FROZEN_CELL_MASK : Z_INDEX_CELL_MASK;
|
||||
return { width, left, top, height: rowHeight, zIndex };
|
||||
}
|
||||
return defaultDimensions;
|
||||
};
|
||||
|
||||
export function getNewSelectedRange(startCell, nextCellPosition) {
|
||||
const { idx: currentIdx, rowIdx: currentRowIdx, groupRecordIndex: currentGroupRecordIndex } = startCell;
|
||||
const { idx: newIdx, rowIdx: newRowIdx, groupRecordIndex: newGroupRecordIndex } = nextCellPosition;
|
||||
const colIndexes = [currentIdx, newIdx].sort((a, b) => a - b);
|
||||
const rowIndexes = [currentRowIdx, newRowIdx].sort((a, b) => a - b);
|
||||
const groupRecordIndexes = [currentGroupRecordIndex, newGroupRecordIndex].sort((a, b) => a - b);
|
||||
const topLeft = { idx: colIndexes[0], rowIdx: rowIndexes[0], groupRecordIndex: groupRecordIndexes[0] };
|
||||
const bottomRight = { idx: colIndexes[1], rowIdx: rowIndexes[1], groupRecordIndex: groupRecordIndexes[1] };
|
||||
return { topLeft, bottomRight };
|
||||
}
|
||||
|
||||
const getColumnRangeProperties = (from, to, columns) => {
|
||||
let totalWidth = 0;
|
||||
let anyColFrozen = false;
|
||||
for (let i = from; i <= to; i++) {
|
||||
const column = columns[i];
|
||||
if (column) {
|
||||
totalWidth += column.width;
|
||||
anyColFrozen = anyColFrozen || column.frozen;
|
||||
}
|
||||
}
|
||||
return { totalWidth, anyColFrozen, left: columns[from].left };
|
||||
};
|
||||
|
||||
export const getSelectedRangeDimensions = ({
|
||||
selectedRange, columns, rowHeight, isGroupView, groups, groupMetrics,
|
||||
groupOffsetLeft, getRecordTopFromRecordsBody,
|
||||
}) => {
|
||||
const { topLeft, bottomRight, startCell, cursorCell } = selectedRange;
|
||||
if (topLeft.idx < 0) {
|
||||
return { width: 0, left: 0, top: 0, height: rowHeight, zIndex: Z_INDEX_CELL_MASK };
|
||||
}
|
||||
|
||||
let { totalWidth, anyColFrozen, left } = getColumnRangeProperties(topLeft.idx, bottomRight.idx, columns);
|
||||
let height;
|
||||
let top;
|
||||
if (isGroupView) {
|
||||
let { groupRecordIndex: startGroupRecordIndex } = startCell;
|
||||
let { groupRecordIndex: endGroupRecordIndex } = cursorCell;
|
||||
const startGroupRow = getGroupRecordByIndex(startGroupRecordIndex, groupMetrics);
|
||||
const endGroupRow = getGroupRecordByIndex(endGroupRecordIndex, groupMetrics);
|
||||
const startGroupPathString = startGroupRow.groupPathString;
|
||||
const endGroupPathString = endGroupRow.groupPathString;
|
||||
let topGroupRowIndex;
|
||||
let selectDirection;
|
||||
if (startGroupRecordIndex < endGroupRecordIndex) {
|
||||
topGroupRowIndex = startGroupRecordIndex;
|
||||
selectDirection = SELECT_DIRECTION.DOWN;
|
||||
} else {
|
||||
topGroupRowIndex = endGroupRecordIndex;
|
||||
selectDirection = SELECT_DIRECTION.UP;
|
||||
}
|
||||
|
||||
if (startGroupPathString === endGroupPathString) {
|
||||
// within the same group.
|
||||
height = (Math.abs(endGroupRecordIndex - startGroupRecordIndex) + 1) * rowHeight;
|
||||
} else if (selectDirection === SELECT_DIRECTION.DOWN) {
|
||||
// within different group: select cells from top to bottom.
|
||||
const groupPath = startGroupRow.groupPath;
|
||||
const group = getGroupByPath(groupPath, groups);
|
||||
const groupRowIds = group.row_ids || [];
|
||||
height = (groupRowIds.length - startGroupRow.rowIdx || 0) * rowHeight;
|
||||
} else if (selectDirection === SELECT_DIRECTION.UP) {
|
||||
// within different group: select cells from bottom to top.
|
||||
const startGroupRowIdx = startGroupRow.rowIdx || 0;
|
||||
topGroupRowIndex = startGroupRecordIndex - startGroupRowIdx;
|
||||
height = (startGroupRowIdx + 1) * rowHeight;
|
||||
}
|
||||
height += 1; // record height: 32
|
||||
left += groupOffsetLeft;
|
||||
top = getRecordTopFromRecordsBody(topGroupRowIndex);
|
||||
} else {
|
||||
height = (bottomRight.rowIdx - topLeft.rowIdx + 1) * rowHeight;
|
||||
top = getRecordTopFromRecordsBody(topLeft.rowIdx);
|
||||
}
|
||||
|
||||
const zIndex = anyColFrozen ? Z_INDEX_FROZEN_CELL_MASK : Z_INDEX_CELL_MASK;
|
||||
return { width: totalWidth, left, top, height, zIndex };
|
||||
};
|
||||
|
||||
export const getRecordsFromSelectedRange = ({ selectedRange, isGroupView, recordGetterByIndex }) => {
|
||||
const { topLeft, bottomRight } = selectedRange;
|
||||
const { rowIdx: startRecordIdx, groupRecordIndex } = topLeft;
|
||||
const { rowIdx: endRecordIdx } = bottomRight;
|
||||
let currentGroupRowIndex = groupRecordIndex;
|
||||
let records = [];
|
||||
for (let recordIndex = startRecordIdx, endIdx = endRecordIdx + 1; recordIndex < endIdx; recordIndex++) {
|
||||
const record = recordGetterByIndex({ isGroupView, groupRecordIndex: currentGroupRowIndex, recordIndex });
|
||||
if (isGroupView) {
|
||||
currentGroupRowIndex++;
|
||||
}
|
||||
if (record) {
|
||||
records.push(record);
|
||||
}
|
||||
}
|
||||
return records;
|
||||
};
|
120
frontend/src/components/sf-table/utils/set-event-transfer.js
Normal file
120
frontend/src/components/sf-table/utils/set-event-transfer.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import TRANSFER_TYPES from '../constants/transfer-types';
|
||||
import { getColumnByIndex } from './column';
|
||||
import { toggleSelection } from './toggle-selection';
|
||||
|
||||
const { TEXT, FRAGMENT } = TRANSFER_TYPES;
|
||||
|
||||
function setEventTransfer({
|
||||
type, selectedRecordIds, copiedRange, copiedColumns, copiedRecords, copiedTableId, tableData, copiedText,
|
||||
recordGetterById, isGroupView, recordGetterByIndex, getClientCellValueDisplayString, event = {},
|
||||
}) {
|
||||
const transfer = event.dataTransfer || event.clipboardData;
|
||||
if (type === TRANSFER_TYPES.METADATA_FRAGMENT) {
|
||||
const copiedText = Array.isArray(selectedRecordIds) && selectedRecordIds.length > 0 ?
|
||||
getCopiedTextFormSelectedRecordIds(selectedRecordIds, tableData, recordGetterById, getClientCellValueDisplayString) :
|
||||
getCopiedTextFromSelectedCells(copiedRange, tableData, isGroupView, recordGetterByIndex, getClientCellValueDisplayString);
|
||||
const copiedGrid = {
|
||||
selectedRecordIds,
|
||||
copiedRange,
|
||||
copiedColumns,
|
||||
copiedRecords,
|
||||
copiedTableId,
|
||||
};
|
||||
const serializeCopiedGrid = JSON.stringify(copiedGrid);
|
||||
if (transfer) {
|
||||
transfer.setData(TEXT, copiedText);
|
||||
transfer.setData(FRAGMENT, serializeCopiedGrid);
|
||||
} else {
|
||||
execCopyWithNoEvents(copiedText, serializeCopiedGrid);
|
||||
}
|
||||
} else {
|
||||
let format = TRANSFER_TYPES[type.toUpperCase()];
|
||||
if (transfer) {
|
||||
transfer.setData(format, copiedText);
|
||||
} else {
|
||||
execCopyWithNoEvents(copiedText, { format });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCopiedTextFormSelectedRecordIds(selectedRecordIds, tableData, recordGetterById, getClientCellValueDisplayString) {
|
||||
const records = selectedRecordIds.map(recordId => recordGetterById(recordId));
|
||||
return getCopiedText(records, tableData.columns, getClientCellValueDisplayString);
|
||||
}
|
||||
|
||||
function getCopiedTextFromSelectedCells(copiedRange, tableData, isGroupView, recordGetterByIndex, getClientCellValueDisplayString) {
|
||||
const { topLeft, bottomRight } = copiedRange;
|
||||
const { rowIdx: minRecordIndex, idx: minColumnIndex, groupRecordIndex } = topLeft;
|
||||
const { rowIdx: maxRecordIndex, idx: maxColumnIndex } = bottomRight;
|
||||
const { columns } = tableData;
|
||||
let currentGroupRecordIndex = groupRecordIndex;
|
||||
let operateRecords = [];
|
||||
let operateColumns = [];
|
||||
for (let i = minRecordIndex; i <= maxRecordIndex; i++) {
|
||||
operateRecords.push(recordGetterByIndex({ isGroupView, groupRecordIndex: currentGroupRecordIndex, recordIndex: i }));
|
||||
if (isGroupView) {
|
||||
currentGroupRecordIndex++;
|
||||
}
|
||||
}
|
||||
for (let i = minColumnIndex; i <= maxColumnIndex; i++) {
|
||||
operateColumns.push(getColumnByIndex(i, columns));
|
||||
}
|
||||
return getCopiedText(operateRecords, operateColumns, getClientCellValueDisplayString);
|
||||
}
|
||||
|
||||
function getCopiedText(records, columns, getClientCellValueDisplayString) {
|
||||
const lastRecordIndex = records.length - 1;
|
||||
const lastColumnIndex = columns.length - 1;
|
||||
let copiedText = '';
|
||||
records.forEach((record, recordIndex) => {
|
||||
columns.forEach((column, columnIndex) => {
|
||||
copiedText += (record && getClientCellValueDisplayString && getClientCellValueDisplayString(record, column)) || '';
|
||||
if (columnIndex < lastColumnIndex) {
|
||||
copiedText += '\t';
|
||||
}
|
||||
});
|
||||
if (recordIndex < lastRecordIndex) {
|
||||
copiedText += '\n';
|
||||
}
|
||||
});
|
||||
return copiedText;
|
||||
}
|
||||
|
||||
export function execCopyWithNoEvents(text, serializeContent) {
|
||||
let reselectPrevious;
|
||||
let range;
|
||||
let selection;
|
||||
let mark;
|
||||
let success = false;
|
||||
try {
|
||||
reselectPrevious = toggleSelection();
|
||||
range = document.createRange();
|
||||
selection = document.getSelection();
|
||||
mark = document.createElement('span');
|
||||
mark.textContent = text;
|
||||
mark.addEventListener('copy', function (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
let transfer = e.dataTransfer || e.clipboardData;
|
||||
transfer.clearData();
|
||||
transfer.setData(TEXT, text);
|
||||
transfer.setData(FRAGMENT, serializeContent);
|
||||
});
|
||||
document.body.appendChild(mark);
|
||||
range.selectNodeContents(mark);
|
||||
selection.addRange(range);
|
||||
success = document.execCommand('copy');
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
if (mark) {
|
||||
document.body.removeChild(mark);
|
||||
}
|
||||
reselectPrevious();
|
||||
}
|
||||
}
|
||||
|
||||
export default setEventTransfer;
|
21
frontend/src/components/sf-table/utils/table.js
Normal file
21
frontend/src/components/sf-table/utils/table.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Get table row by id
|
||||
* @param {object} table
|
||||
* @param {string} rowId the id of row
|
||||
* @returns row, object
|
||||
*/
|
||||
export const getRowById = (table, rowId) => {
|
||||
if (!table || !table.id_row_map || !rowId) return null;
|
||||
return table.id_row_map[rowId];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get table rows by ids
|
||||
* @param {object} table { id_row_map, ... }
|
||||
* @param {array} rowsIds [ row._id, ... ]
|
||||
* @returns rows, array
|
||||
*/
|
||||
export const getRowsByIds = (table, rowsIds) => {
|
||||
if (!table || !table.id_row_map || !Array.isArray(rowsIds)) return [];
|
||||
return rowsIds.map((rowId) => table.id_row_map[rowId]).filter(Boolean);
|
||||
};
|
34
frontend/src/components/sf-table/utils/toggle-selection.js
Normal file
34
frontend/src/components/sf-table/utils/toggle-selection.js
Normal file
@@ -0,0 +1,34 @@
|
||||
export function toggleSelection() {
|
||||
let selection = document.getSelection();
|
||||
if (!selection.rangeCount) {
|
||||
return function () {};
|
||||
}
|
||||
let active = document.activeElement;
|
||||
let ranges = [];
|
||||
for (let i = 0; i < selection.rangeCount; i++) {
|
||||
ranges.push(selection.getRangeAt(i));
|
||||
}
|
||||
|
||||
switch (active.tagName.toUpperCase()) { // .toUpperCase handles XHTML
|
||||
case 'INPUT':
|
||||
case 'TEXTAREA':
|
||||
active.blur();
|
||||
break;
|
||||
default:
|
||||
active = null;
|
||||
break;
|
||||
}
|
||||
|
||||
selection.removeAllRanges();
|
||||
return function () {
|
||||
selection.type === 'Caret' &&
|
||||
selection.removeAllRanges();
|
||||
if (!selection.rangeCount) {
|
||||
ranges.forEach(function (range) {
|
||||
selection.addRange(range);
|
||||
});
|
||||
}
|
||||
active &&
|
||||
active.focus();
|
||||
};
|
||||
}
|
29
frontend/src/components/sf-table/utils/viewport.js
Normal file
29
frontend/src/components/sf-table/utils/viewport.js
Normal file
@@ -0,0 +1,29 @@
|
||||
export const getColVisibleStartIdx = (columns, scrollLeft) => {
|
||||
let remainingScroll = scrollLeft;
|
||||
const nonFrozenColumns = columns.slice(0);
|
||||
for (let i = 0; i < nonFrozenColumns.length; i++) {
|
||||
let { width } = columns[i];
|
||||
remainingScroll -= width;
|
||||
if (remainingScroll < 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getColVisibleEndIdx = (columns, gridWidth, scrollLeft) => {
|
||||
let remainingWidth = gridWidth + scrollLeft;
|
||||
for (let i = 0; i < columns.length; i++) {
|
||||
let { width } = columns[i];
|
||||
remainingWidth -= width;
|
||||
if (remainingWidth < 0) {
|
||||
return i - 1;
|
||||
}
|
||||
}
|
||||
return columns.length - 1;
|
||||
};
|
||||
|
||||
export const getVisibleBoundaries = (columns, scrollLeft, gridWidth) => {
|
||||
const colVisibleStartIdx = getColVisibleStartIdx(columns, scrollLeft);
|
||||
const colVisibleEndIdx = getColVisibleEndIdx(columns, gridWidth, scrollLeft);
|
||||
return { colVisibleStartIdx, colVisibleEndIdx };
|
||||
};
|
Reference in New Issue
Block a user