1
0
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:
Jerry Ren
2025-01-07 12:17:57 +08:00
committed by GitHub
parent 0c0f07014b
commit 2cc16bdf11
120 changed files with 11263 additions and 516 deletions

View 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',
};

View 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',
};

View 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;

View 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;

View 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;

View File

@@ -0,0 +1,6 @@
.sf-table-context-menu {
display: block;
opacity: 1;
box-shadow: 0 0 5px #ccc;
position: fixed;
}

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View 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;

View 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;
}

View 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;

View 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;

View 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;

View 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;

View File

@@ -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;
}

File diff suppressed because it is too large Load Diff

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,9 @@
import RightScrollbar from './right-scrollbar';
import HorizontalScrollbar from './horizontal-scrollbar';
import './scrollbar.css';
export {
RightScrollbar,
HorizontalScrollbar,
};

View File

@@ -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;

View 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;
}

View 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;

View 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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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

View 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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View 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
);
};

View 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];
};

View 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);
};

View 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;

View 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;

View 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);
};

View 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] || {};
};

View 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;
};

View 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');
};

View 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;

View 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;
};

View 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;
};

View 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;

View 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);
};

View 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();
};
}

View 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 };
};