mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-09 10:50:24 +00:00
Add context menu (#6519)
* add context menu component, implement open in new tab and open parent folder options * clean redundant change * update context menu to support group mode * remove redundant code * improve ui component style
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
.sf-metadata-contextmenu {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: 0 0 5px #ccc;
|
||||||
|
}
|
@@ -0,0 +1,118 @@
|
|||||||
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import './context-menu.css';
|
||||||
|
|
||||||
|
const ContextMenu = ({ options, onOptionClick }) => {
|
||||||
|
const menuRef = useRef(null);
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
||||||
|
|
||||||
|
const handleHide = useCallback((event) => {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
}, [menuRef]);
|
||||||
|
|
||||||
|
const handleOptionClick = (event, option) => {
|
||||||
|
onOptionClick(event, option);
|
||||||
|
setVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMenuPosition = (x = 0, y = 0) => {
|
||||||
|
let menuStyles = {
|
||||||
|
top: y,
|
||||||
|
left: x
|
||||||
|
};
|
||||||
|
if (!menuRef.current) return menuStyles;
|
||||||
|
|
||||||
|
const { innerWidth, innerHeight } = window;
|
||||||
|
const rect = menuRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Calculate the offset of the parent components
|
||||||
|
const parentRect = menuRef.current.parentElement.getBoundingClientRect();
|
||||||
|
const offsetX = parentRect.left;
|
||||||
|
const offsetY = parentRect.top;
|
||||||
|
|
||||||
|
// Adjust the position based on the offset
|
||||||
|
menuStyles.top = y - offsetY;
|
||||||
|
menuStyles.left = x - offsetX;
|
||||||
|
|
||||||
|
const metadataResultFooterHeight = 32;
|
||||||
|
const contentHeight = innerHeight - metadataResultFooterHeight;
|
||||||
|
if (y + rect.height > contentHeight) {
|
||||||
|
menuStyles.top -= rect.height;
|
||||||
|
}
|
||||||
|
if (x + rect.width > innerWidth) {
|
||||||
|
menuStyles.left -= rect.width;
|
||||||
|
}
|
||||||
|
if (menuStyles.top < 0) {
|
||||||
|
menuStyles.top = rect.height < contentHeight ? (contentHeight - rect.height) / 2 : 0;
|
||||||
|
}
|
||||||
|
if (menuStyles.left < 0) {
|
||||||
|
menuStyles.left = rect.width < innerWidth ? (innerWidth - rect.width) / 2 : 0;
|
||||||
|
}
|
||||||
|
return menuStyles;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleShow = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (menuRef.current && menuRef.current.contains(event.target)) return;
|
||||||
|
|
||||||
|
setVisible(true);
|
||||||
|
|
||||||
|
const position = getMenuPosition(event.clientX, event.clientY);
|
||||||
|
setPosition(position);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('contextmenu', handleShow);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('contextmenu', handleShow);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) {
|
||||||
|
document.addEventListener('mousedown', handleHide);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('mousedown', handleHide);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleHide);
|
||||||
|
};
|
||||||
|
}, [visible, handleHide]);
|
||||||
|
|
||||||
|
if (!visible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
className='dropdown-menu sf-metadata-contextmenu'
|
||||||
|
style={position}
|
||||||
|
>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className='dropdown-item sf-metadata-contextmenu-item'
|
||||||
|
onClick={(event) => handleOptionClick(event, option)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ContextMenu.propTypes = {
|
||||||
|
options: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
})
|
||||||
|
).isRequired,
|
||||||
|
onOptionClick: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ContextMenu;
|
@@ -28,7 +28,6 @@ class RecordsBody extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
startRenderIndex: 0,
|
startRenderIndex: 0,
|
||||||
endRenderIndex: this.getInitEndIndex(props),
|
endRenderIndex: this.getInitEndIndex(props),
|
||||||
isContextMenuShow: false,
|
|
||||||
activeRecords: [],
|
activeRecords: [],
|
||||||
menuPosition: null,
|
menuPosition: null,
|
||||||
selectedPosition: null,
|
selectedPosition: null,
|
||||||
@@ -47,7 +46,6 @@ class RecordsBody extends Component {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.onRef(this);
|
this.props.onRef(this);
|
||||||
window.sfMetadataBody = this;
|
window.sfMetadataBody = this;
|
||||||
document.addEventListener('contextmenu', this.handleContextMenu);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
@@ -58,8 +56,6 @@ class RecordsBody extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('contextmenu', this.handleContextMenu);
|
|
||||||
|
|
||||||
this.clearHorizontalScroll();
|
this.clearHorizontalScroll();
|
||||||
this.clearScrollbarTimer();
|
this.clearScrollbarTimer();
|
||||||
this.setState = (state, callback) => {
|
this.setState = (state, callback) => {
|
||||||
@@ -335,6 +331,10 @@ class RecordsBody extends Component {
|
|||||||
this.props.onCellRangeSelectionUpdated(selectedRange);
|
this.props.onCellRangeSelectionUpdated(selectedRange);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onCellContextMenu = (event, cell) => {
|
||||||
|
this.props.onCellContextMenu(event, cell);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When updating the selection by moving the mouse, you need to automatically scroll to expand the visible area
|
* When updating the selection by moving the mouse, you need to automatically scroll to expand the visible area
|
||||||
* @param {object} selectedRange
|
* @param {object} selectedRange
|
||||||
@@ -414,6 +414,7 @@ class RecordsBody extends Component {
|
|||||||
onCellMouseMove: this.onCellMouseMove,
|
onCellMouseMove: this.onCellMouseMove,
|
||||||
onDragEnter: this.handleDragEnter,
|
onDragEnter: this.handleDragEnter,
|
||||||
modifyRecord: this.props.modifyRecord,
|
modifyRecord: this.props.modifyRecord,
|
||||||
|
onCellContextMenu: this.onCellContextMenu
|
||||||
};
|
};
|
||||||
return this.cellMetaData;
|
return this.cellMetaData;
|
||||||
};
|
};
|
||||||
@@ -504,7 +505,6 @@ class RecordsBody extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// const { isContextMenuShow, menuPosition, activeRecords } = this.state;
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div id="canvas" className="sf-metadata-result-table-content" ref={this.setResultContentRef} onScroll={this.onScroll}>
|
<div id="canvas" className="sf-metadata-result-table-content" ref={this.setResultContentRef} onScroll={this.onScroll}>
|
||||||
@@ -605,6 +605,7 @@ RecordsBody.propTypes = {
|
|||||||
getCopiedRecordsAndColumnsFromRange: PropTypes.func,
|
getCopiedRecordsAndColumnsFromRange: PropTypes.func,
|
||||||
openDownloadFilesDialog: PropTypes.func,
|
openDownloadFilesDialog: PropTypes.func,
|
||||||
cacheDownloadFilesProps: PropTypes.func,
|
cacheDownloadFilesProps: PropTypes.func,
|
||||||
|
onCellContextMenu: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RecordsBody;
|
export default RecordsBody;
|
||||||
|
@@ -305,6 +305,7 @@ class GroupBody extends Component {
|
|||||||
onCellMouseMove: this.onCellMouseMove,
|
onCellMouseMove: this.onCellMouseMove,
|
||||||
onDragEnter: this.handleDragEnter,
|
onDragEnter: this.handleDragEnter,
|
||||||
modifyRecord: this.props.modifyRecord,
|
modifyRecord: this.props.modifyRecord,
|
||||||
|
onCellContextMenu: this.onCellContextMenu,
|
||||||
};
|
};
|
||||||
return this.cellMetaData;
|
return this.cellMetaData;
|
||||||
};
|
};
|
||||||
@@ -445,6 +446,10 @@ class GroupBody extends Component {
|
|||||||
this.selectUpdate(cellPosition, false, this.updateViewableArea);
|
this.selectUpdate(cellPosition, false, this.updateViewableArea);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onCellContextMenu = (event, cell) => {
|
||||||
|
this.props.onCellContextMenu(event, cell);
|
||||||
|
};
|
||||||
|
|
||||||
onWindowMouseUp = (event) => {
|
onWindowMouseUp = (event) => {
|
||||||
window.removeEventListener('mouseup', this.onWindowMouseUp);
|
window.removeEventListener('mouseup', this.onWindowMouseUp);
|
||||||
if (isShiftKeyDown(event)) return;
|
if (isShiftKeyDown(event)) return;
|
||||||
@@ -966,6 +971,7 @@ GroupBody.propTypes = {
|
|||||||
getCopiedRecordsAndColumnsFromRange: PropTypes.func,
|
getCopiedRecordsAndColumnsFromRange: PropTypes.func,
|
||||||
openDownloadFilesDialog: PropTypes.func,
|
openDownloadFilesDialog: PropTypes.func,
|
||||||
cacheDownloadFilesProps: PropTypes.func,
|
cacheDownloadFilesProps: PropTypes.func,
|
||||||
|
onCellContextMenu: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GroupBody;
|
export default GroupBody;
|
||||||
|
@@ -15,6 +15,10 @@ import RecordMetrics from '../../../../utils/record-metrics';
|
|||||||
import { isShiftKeyDown } from '../../../../utils/keyboard-utils';
|
import { isShiftKeyDown } from '../../../../utils/keyboard-utils';
|
||||||
import { getVisibleBoundaries } from '../../../../utils/viewport';
|
import { getVisibleBoundaries } from '../../../../utils/viewport';
|
||||||
import { getColOverScanEndIdx, getColOverScanStartIdx } from '../../../../utils/grid';
|
import { getColOverScanEndIdx, getColOverScanStartIdx } from '../../../../utils/grid';
|
||||||
|
import TextTranslation from '../../../../../../utils/text-translation';
|
||||||
|
import { Utils } from '../../../../../../utils/utils';
|
||||||
|
import { siteRoot } from '../../../../../../utils/constants';
|
||||||
|
import ContextMenu from '../../../context-menu/context-menu';
|
||||||
|
|
||||||
class Records extends Component {
|
class Records extends Component {
|
||||||
|
|
||||||
@@ -38,10 +42,16 @@ class Records extends Component {
|
|||||||
topLeft: this.initPosition,
|
topLeft: this.initPosition,
|
||||||
bottomRight: this.initPosition,
|
bottomRight: this.initPosition,
|
||||||
},
|
},
|
||||||
|
selectedPosition: this.initPosition,
|
||||||
...initHorizontalScrollState,
|
...initHorizontalScrollState,
|
||||||
};
|
};
|
||||||
this.isWindows = isWindowsBrowser();
|
this.isWindows = isWindowsBrowser();
|
||||||
this.isWebkit = isWebkitBrowser();
|
this.isWebkit = isWebkitBrowser();
|
||||||
|
this.baseURI = '';
|
||||||
|
this.contextMenuOptions = [
|
||||||
|
{ label: TextTranslation.OPEN_FILE_IN_NEW_TAB.value, value: 'openFileInNewTab' },
|
||||||
|
{ label: TextTranslation.OPEN_PARENT_FOLDER.value, value: 'openParentFolder' },
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -587,6 +597,56 @@ class Records extends Component {
|
|||||||
this.setState(scrollState);
|
this.setState(scrollState);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onOpenFileInNewTab = () => {
|
||||||
|
const { isGroupView, recordGetterByIndex } = this.props;
|
||||||
|
const { groupRecordIndex, rowIdx } = this.state.selectedPosition;
|
||||||
|
const record = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex: rowIdx });
|
||||||
|
const repoID = window.sfMetadataStore.repoId;
|
||||||
|
let url;
|
||||||
|
if (record._is_dir) {
|
||||||
|
url = `${this.baseURI}${record._parent_dir === '/' ? '' : record._parent_dir}/${record._name}`;
|
||||||
|
} else {
|
||||||
|
url = `${siteRoot}lib/${repoID}/file${Utils.encodePath(record._parent_dir + '/' + record._name)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(url, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
onOpenParentFolder = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const { isGroupView, recordGetterByIndex } = this.props;
|
||||||
|
const { groupRecordIndex, rowIdx } = this.state.selectedPosition;
|
||||||
|
const record = recordGetterByIndex({ isGroupView, groupRecordIndex, recordIndex: rowIdx });
|
||||||
|
const url = `${this.baseURI}${record._parent_dir}`;
|
||||||
|
|
||||||
|
window.open(url, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
onOptionClick = (event, option) => {
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
openFileInNewTab: this.onOpenFileInNewTab.bind(this),
|
||||||
|
openParentFolder: this.onOpenParentFolder.bind(this),
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = handlers[option.value];
|
||||||
|
if (handler) {
|
||||||
|
handler(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onCellContextMenu = (event, cell) => {
|
||||||
|
const url = new URL(event.target.baseURI);
|
||||||
|
url.search = '';
|
||||||
|
this.baseURI = url.toString();
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
selectedPosition: cell,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
renderRecordsBody = ({ containerWidth }) => {
|
renderRecordsBody = ({ containerWidth }) => {
|
||||||
const { recordMetrics, columnMetrics, colOverScanStartIdx, colOverScanEndIdx } = this.state;
|
const { recordMetrics, columnMetrics, colOverScanStartIdx, colOverScanEndIdx } = this.state;
|
||||||
const {
|
const {
|
||||||
@@ -606,6 +666,7 @@ class Records extends Component {
|
|||||||
setRecordsScrollLeft: this.setScrollLeft,
|
setRecordsScrollLeft: this.setScrollLeft,
|
||||||
hasSelectedCell: this.hasSelectedCell,
|
hasSelectedCell: this.hasSelectedCell,
|
||||||
cacheScrollTop: this.storeScrollTop,
|
cacheScrollTop: this.storeScrollTop,
|
||||||
|
onCellContextMenu: this.onCellContextMenu,
|
||||||
};
|
};
|
||||||
if (this.props.isGroupView) {
|
if (this.props.isGroupView) {
|
||||||
return (
|
return (
|
||||||
@@ -667,6 +728,10 @@ class Records extends Component {
|
|||||||
/>
|
/>
|
||||||
{this.renderRecordsBody({ containerWidth })}
|
{this.renderRecordsBody({ containerWidth })}
|
||||||
</div>
|
</div>
|
||||||
|
<ContextMenu
|
||||||
|
options={this.contextMenuOptions}
|
||||||
|
onOptionClick={this.onOptionClick}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{this.isWindows && this.isWebkit && (
|
{this.isWindows && this.isWebkit && (
|
||||||
<HorizontalScrollbar
|
<HorizontalScrollbar
|
||||||
|
@@ -102,6 +102,15 @@ const Cell = React.memo(({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onCellContextMenu = useCallback((event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const cell = { idx: column.idx, groupRecordIndex, rowIdx: recordIndex };
|
||||||
|
if (!isFunction(cellMetaData.onCellContextMenu)) return;
|
||||||
|
cellMetaData.onCellContextMenu(event, cell,);
|
||||||
|
cellMetaData.onCellClick(cell, event);
|
||||||
|
}, [cellMetaData, column, groupRecordIndex, recordIndex]);
|
||||||
|
|
||||||
const getEvents = useCallback(() => {
|
const getEvents = useCallback(() => {
|
||||||
return {
|
return {
|
||||||
onClick: onCellClick,
|
onClick: onCellClick,
|
||||||
@@ -110,9 +119,10 @@ const Cell = React.memo(({
|
|||||||
onMouseEnter: onCellMouseEnter,
|
onMouseEnter: onCellMouseEnter,
|
||||||
onMouseMove: onCellMouseMove,
|
onMouseMove: onCellMouseMove,
|
||||||
onMouseLeave: onCellMouseLeave,
|
onMouseLeave: onCellMouseLeave,
|
||||||
onDragOver: onDragOver
|
onDragOver: onDragOver,
|
||||||
|
onContextMenu: onCellContextMenu,
|
||||||
};
|
};
|
||||||
}, [onCellClick, onCellDoubleClick, onCellMouseDown, onCellMouseEnter, onCellMouseMove, onCellMouseLeave, onDragOver]);
|
}, [onCellClick, onCellDoubleClick, onCellMouseDown, onCellMouseEnter, onCellMouseMove, onCellMouseLeave, onDragOver, onCellContextMenu]);
|
||||||
|
|
||||||
const getOldRowData = useCallback((originalOldCellValue) => {
|
const getOldRowData = useCallback((originalOldCellValue) => {
|
||||||
const { key: columnKey, name: columnName } = column;
|
const { key: columnKey, name: columnName } = column;
|
||||||
|
@@ -19,6 +19,8 @@ const TextTranslation = {
|
|||||||
'PERMISSION': { key: 'Permission', value: gettext('Permission') },
|
'PERMISSION': { key: 'Permission', value: gettext('Permission') },
|
||||||
'DETAILS': { key: 'Details', value: gettext('Details') },
|
'DETAILS': { key: 'Details', value: gettext('Details') },
|
||||||
'OPEN_VIA_CLIENT': { key: 'Open via Client', value: gettext('Open via Client') },
|
'OPEN_VIA_CLIENT': { key: 'Open via Client', value: gettext('Open via Client') },
|
||||||
|
'OPEN_FILE_IN_NEW_TAB': { key: 'Open file in new tab', value: gettext('Open file in new tab') },
|
||||||
|
'OPEN_PARENT_FOLDER': { key: 'Open parent folder', value: gettext('Open parent folder') },
|
||||||
'LOCK': { key: 'Lock', value: gettext('Lock') },
|
'LOCK': { key: 'Lock', value: gettext('Lock') },
|
||||||
'UNLOCK': { key: 'Unlock', value: gettext('Unlock') },
|
'UNLOCK': { key: 'Unlock', value: gettext('Unlock') },
|
||||||
'FREEZE_DOCUMENT': { key: 'Freeze Document', value: gettext('Freeze Document') },
|
'FREEZE_DOCUMENT': { key: 'Freeze Document', value: gettext('Freeze Document') },
|
||||||
|
Reference in New Issue
Block a user