mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-04 08:28:11 +00:00
Grid view support multi-select by drag (#6420)
* grid view support multiple selection by drag * fix conflict with multiple selection by keyboard * improve selection experience and code format * fix bug - select failed when file name include '...'
This commit is contained in:
@@ -65,6 +65,7 @@ const propTypes = {
|
||||
isAllItemSelected: PropTypes.bool.isRequired,
|
||||
onAllItemSelected: PropTypes.func.isRequired,
|
||||
selectedDirentList: PropTypes.array.isRequired,
|
||||
onSelectedDirentListUpdate: PropTypes.func.isRequired,
|
||||
onItemsMove: PropTypes.func.isRequired,
|
||||
onItemsCopy: PropTypes.func.isRequired,
|
||||
onItemsDelete: PropTypes.func.isRequired,
|
||||
@@ -254,6 +255,7 @@ class DirColumnView extends React.Component {
|
||||
direntList={this.props.direntList}
|
||||
fullDirentList={this.props.fullDirentList}
|
||||
selectedDirentList={this.props.selectedDirentList}
|
||||
onSelectedDirentListUpdate={this.props.onSelectedDirentListUpdate}
|
||||
onAddFile={this.props.onAddFile}
|
||||
onItemClick={this.props.onItemClick}
|
||||
onItemDelete={this.props.onItemDelete}
|
||||
|
@@ -12,6 +12,7 @@ const propTypes = {
|
||||
updateUsedRepoTags: PropTypes.func.isRequired,
|
||||
direntList: PropTypes.array.isRequired,
|
||||
selectedDirentList: PropTypes.array.isRequired,
|
||||
onSelectedDirentListUpdate: PropTypes.func.isRequired,
|
||||
onItemClick: PropTypes.func.isRequired,
|
||||
onGridItemClick: PropTypes.func,
|
||||
onAddFile: PropTypes.func.isRequired,
|
||||
@@ -73,6 +74,7 @@ class DirGridView extends React.Component {
|
||||
direntList={this.props.direntList}
|
||||
fullDirentList={this.props.fullDirentList}
|
||||
selectedDirentList={this.props.selectedDirentList}
|
||||
onSelectedDirentListUpdate={this.props.onSelectedDirentListUpdate}
|
||||
onAddFile={this.props.onAddFile}
|
||||
onItemClick={this.props.onItemClick}
|
||||
onItemDelete={this.props.onItemDelete}
|
||||
|
@@ -77,16 +77,16 @@ class DirentGridItem extends React.Component {
|
||||
}
|
||||
|
||||
if (dirent === activeDirent && !event.metaKey && !event.ctrlKey) {
|
||||
this.handleDoubleClick(dirent);
|
||||
this.handleDoubleClick(dirent, event);
|
||||
} else {
|
||||
this.props.onGridItemClick(dirent, event);
|
||||
}
|
||||
};
|
||||
|
||||
handleDoubleClick = (dirent) => {
|
||||
handleDoubleClick = (dirent, event) => {
|
||||
if (Utils.imageCheck(dirent.name)) {
|
||||
this.props.showImagePopup(dirent);
|
||||
this.props.onGridItemClick(null);
|
||||
this.props.onGridItemClick(null, event);
|
||||
} else {
|
||||
this.props.onItemClick(dirent);
|
||||
}
|
||||
@@ -106,7 +106,7 @@ class DirentGridItem extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleDoubleClick(dirent);
|
||||
this.handleDoubleClick(dirent, e);
|
||||
};
|
||||
|
||||
onGridItemDragStart = (e) => {
|
||||
|
@@ -31,6 +31,7 @@ const propTypes = {
|
||||
direntList: PropTypes.array.isRequired,
|
||||
fullDirentList: PropTypes.array,
|
||||
selectedDirentList: PropTypes.array.isRequired,
|
||||
onSelectedDirentListUpdate: PropTypes.func.isRequired,
|
||||
onAddFile: PropTypes.func,
|
||||
onItemDelete: PropTypes.func,
|
||||
onItemCopy: PropTypes.func.isRequired,
|
||||
@@ -86,10 +87,160 @@ class DirentGridView extends React.Component {
|
||||
isGridItemFreezed: false,
|
||||
activeDirent: null,
|
||||
downloadItems: [],
|
||||
|
||||
startPoint: { x: 0, y: 0 },
|
||||
endPoint: { x: 0, y: 0 },
|
||||
selectedItemsList: [],
|
||||
isSelecting: false,
|
||||
isMouseDown: false,
|
||||
autoScrollInterval: null,
|
||||
};
|
||||
this.containerRef = React.createRef();
|
||||
this.isRepoOwner = props.currentRepoInfo.owner_email === username;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('mouseup', this.onGlobalMouseUp);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('mouseup', this.onGlobalMouseUp);
|
||||
}
|
||||
|
||||
onGridContainerMouseDown = (event) => {
|
||||
if (event.button === 2) {
|
||||
return;
|
||||
} else if (event.button === 0) {
|
||||
hideMenu();
|
||||
this.props.onGridItemClick(null);
|
||||
if (event.target.closest('img') || event.target.closest('div.grid-file-name')) return;
|
||||
|
||||
const containerBounds = this.containerRef.current.getBoundingClientRect();
|
||||
this.setState({
|
||||
startPoint: { x: event.clientX - containerBounds.left, y: event.clientY - containerBounds.top },
|
||||
endPoint: { x: event.clientX - containerBounds.left, y: event.clientY - containerBounds.top },
|
||||
selectedItemsList: [],
|
||||
isSelecting: false,
|
||||
isMouseDown: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onSelectMouseMove = (e) => {
|
||||
if (!this.state.isMouseDown) return;
|
||||
|
||||
const containerBounds = this.containerRef.current.getBoundingClientRect();
|
||||
const endPoint = { x: e.clientX - containerBounds.left, y: e.clientY - containerBounds.top };
|
||||
|
||||
// Constrain endPoint within the container bounds
|
||||
endPoint.x = Math.max(0, Math.min(endPoint.x, containerBounds.width));
|
||||
endPoint.y = Math.max(0, Math.min(endPoint.y, containerBounds.height));
|
||||
|
||||
// Check if the mouse has moved a certain distance to start selection, prevents accidental selections
|
||||
const distance = Math.sqrt(
|
||||
Math.pow(endPoint.x - this.state.startPoint.x, 2) +
|
||||
Math.pow(endPoint.y - this.state.startPoint.y, 2)
|
||||
);
|
||||
if (distance > 5) {
|
||||
this.setState({
|
||||
isSelecting: true,
|
||||
endPoint: endPoint,
|
||||
}, () => {
|
||||
this.determineSelectedItems();
|
||||
this.autoScroll(e.clientY);
|
||||
|
||||
const selectedItemNames = new Set(this.state.selectedItemsList.map(item => item.lastChild.lastChild.title));
|
||||
const filteredDirentList = this.props.direntList
|
||||
.filter(dirent => selectedItemNames.has(dirent.name))
|
||||
.map(dirent => ({ ...dirent, isSelected: true }));
|
||||
|
||||
this.props.onSelectedDirentListUpdate(filteredDirentList);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onGlobalMouseUp = () => {
|
||||
if (!this.state.isMouseDown) return;
|
||||
clearInterval(this.state.autoScrollInterval);
|
||||
this.setState({
|
||||
isSelecting: false,
|
||||
isMouseDown: false,
|
||||
autoScrollInterval: null,
|
||||
});
|
||||
};
|
||||
|
||||
determineSelectedItems = () => {
|
||||
const { startPoint, endPoint } = this.state;
|
||||
const container = this.containerRef.current;
|
||||
const items = container.querySelectorAll('.grid-item');
|
||||
|
||||
const selectionRect = {
|
||||
left: Math.min(startPoint.x, endPoint.x),
|
||||
top: Math.min(startPoint.y, endPoint.y),
|
||||
right: Math.max(startPoint.x, endPoint.x),
|
||||
bottom: Math.max(startPoint.y, endPoint.y),
|
||||
};
|
||||
|
||||
const newSelectedItemsList = [];
|
||||
|
||||
items.forEach(item => {
|
||||
const bounds = item.getBoundingClientRect();
|
||||
const relativeBounds = {
|
||||
left: bounds.left - container.getBoundingClientRect().left,
|
||||
top: bounds.top - container.getBoundingClientRect().top,
|
||||
right: bounds.right - container.getBoundingClientRect().left,
|
||||
bottom: bounds.bottom - container.getBoundingClientRect().top,
|
||||
};
|
||||
|
||||
// Check if the element is within the selection box's bounds
|
||||
if (relativeBounds.left < selectionRect.right && relativeBounds.right > selectionRect.left &&
|
||||
relativeBounds.top < selectionRect.bottom && relativeBounds.bottom > selectionRect.top) {
|
||||
newSelectedItemsList.push(item);
|
||||
}
|
||||
});
|
||||
this.setState({ selectedItemsList: newSelectedItemsList });
|
||||
};
|
||||
|
||||
autoScroll = (mouseY) => {
|
||||
const container = this.containerRef.current;
|
||||
const containerBounds = container.getBoundingClientRect();
|
||||
const scrollSpeed = 10;
|
||||
const scrollThreshold = 20;
|
||||
|
||||
const updateEndPoint = () => {
|
||||
const endPoint = {
|
||||
x: this.state.endPoint.x,
|
||||
y: mouseY - containerBounds.top + container.scrollTop,
|
||||
};
|
||||
this.setState({ endPoint }, () => {
|
||||
this.determineSelectedItems();
|
||||
});
|
||||
};
|
||||
|
||||
if (mouseY < containerBounds.top + scrollThreshold) {
|
||||
// Scroll Up
|
||||
if (!this.state.autoScrollInterval) {
|
||||
const interval = setInterval(() => {
|
||||
container.scrollTop -= scrollSpeed;
|
||||
updateEndPoint();
|
||||
}, 50);
|
||||
this.setState({ autoScrollInterval: interval });
|
||||
}
|
||||
} else if (mouseY > containerBounds.bottom - scrollThreshold) {
|
||||
// Scroll Down
|
||||
if (!this.state.autoScrollInterval) {
|
||||
const interval = setInterval(() => {
|
||||
container.scrollTop += scrollSpeed;
|
||||
updateEndPoint();
|
||||
}, 50);
|
||||
this.setState({ autoScrollInterval: interval });
|
||||
}
|
||||
} else {
|
||||
clearInterval(this.state.autoScrollInterval);
|
||||
this.setState({ autoScrollInterval: null });
|
||||
}
|
||||
};
|
||||
|
||||
onCreateFileToggle = (fileType) => {
|
||||
this.setState({
|
||||
isCreateFileDialogShow: !this.state.isCreateFileDialogShow,
|
||||
@@ -444,27 +595,17 @@ class DirentGridView extends React.Component {
|
||||
return Utils.checkDuplicatedNameInList(this.props.direntList, newName);
|
||||
};
|
||||
|
||||
// common contextmenu handle
|
||||
onMouseDown = (event) => {
|
||||
onGridItemMouseDown = (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (event.button === 2) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
onGridContainerMouseDown = (event) => {
|
||||
this.onMouseDown(event);
|
||||
};
|
||||
|
||||
onGridItemMouseDown = (event) => {
|
||||
this.onMouseDown(event);
|
||||
};
|
||||
|
||||
gridContainerClick = () => {
|
||||
gridContainerClick = (event) => {
|
||||
event.stopPropagation();
|
||||
hideMenu();
|
||||
if (!this.props.isDirentDetailShow) {
|
||||
this.onGridItemClick(null);
|
||||
}
|
||||
};
|
||||
|
||||
onGridContainerContextMenu = (event) => {
|
||||
@@ -509,7 +650,7 @@ class DirentGridView extends React.Component {
|
||||
let menuList = Utils.getDirentOperationList(this.isRepoOwner, currentRepoInfo, selectedDirentList[0], true);
|
||||
this.handleContextClick(event, GRID_ITEM_CONTEXTMENU_ID, menuList, selectedDirentList[0]);
|
||||
} else {
|
||||
this.onDirentClick(null);
|
||||
this.props.onGridItemClick(null);
|
||||
event.persist();
|
||||
if (!hasCustomPermission('modify')) return;
|
||||
setTimeout(() => {
|
||||
@@ -577,6 +718,21 @@ class DirentGridView extends React.Component {
|
||||
return Utils.getDirentOperationList(isRepoOwner, currentRepoInfo, dirent, isContextmenu);
|
||||
};
|
||||
|
||||
renderSelectionBox = () => {
|
||||
const { startPoint, endPoint } = this.state;
|
||||
if (!this.state.isSelecting) return null;
|
||||
const left = Math.min(startPoint.x, endPoint.x);
|
||||
const top = Math.min(startPoint.y, endPoint.y);
|
||||
const width = Math.abs(startPoint.x - endPoint.x);
|
||||
const height = Math.abs(startPoint.y - endPoint.y);
|
||||
return (
|
||||
<div
|
||||
className="selection-box"
|
||||
style={{ left, top, width, height }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
let { direntList, selectedDirentList, path } = this.props;
|
||||
let dirent = this.state.activeDirent ? this.state.activeDirent : '';
|
||||
@@ -588,7 +744,14 @@ class DirentGridView extends React.Component {
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<ul className="grid-view" onClick={this.gridContainerClick} onContextMenu={this.onGridContainerContextMenu} onMouseDown={this.onGridContainerMouseDown}>
|
||||
<ul
|
||||
className="grid-view"
|
||||
onClick={this.gridContainerClick}
|
||||
onContextMenu={this.onGridContainerContextMenu}
|
||||
onMouseDown={this.onGridContainerMouseDown}
|
||||
onMouseMove={this.onSelectMouseMove}
|
||||
ref={this.containerRef}
|
||||
>
|
||||
{
|
||||
direntList.length !== 0 && direntList.map((dirent, index) => {
|
||||
return (
|
||||
@@ -609,6 +772,7 @@ class DirentGridView extends React.Component {
|
||||
);
|
||||
})
|
||||
}
|
||||
{this.renderSelectionBox()}
|
||||
</ul>
|
||||
<ContextMenu
|
||||
id={GRID_ITEM_CONTEXTMENU_ID}
|
||||
|
@@ -46,6 +46,7 @@ class ViewFileToolbar extends React.Component {
|
||||
};
|
||||
|
||||
onDropDownMouseMove = (e) => {
|
||||
e.preventDefault();
|
||||
if (this.state.isSubMenuShown && e.target && e.target.className === 'dropdown-item') {
|
||||
this.setState({
|
||||
isSubMenuShown: false
|
||||
|
@@ -85,3 +85,9 @@
|
||||
.grid-drop-show {
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.selection-box {
|
||||
position: absolute;
|
||||
background-color: rgba(0, 120, 215, 0.3);
|
||||
border: 1px solid rgba(0, 120, 215, 0.8);
|
||||
}
|
||||
|
@@ -56,6 +56,7 @@
|
||||
}
|
||||
|
||||
.dir-content-main {
|
||||
position: relative;
|
||||
flex: 1 0 74.5%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@@ -81,6 +81,7 @@ const propTypes = {
|
||||
isDirentDetailShow: PropTypes.bool.isRequired,
|
||||
selectedDirent: PropTypes.object,
|
||||
selectedDirentList: PropTypes.array.isRequired,
|
||||
onSelectedDirentListUpdate: PropTypes.func.isRequired,
|
||||
onItemsMove: PropTypes.func.isRequired,
|
||||
onItemsCopy: PropTypes.func.isRequired,
|
||||
onItemsDelete: PropTypes.func.isRequired,
|
||||
@@ -305,6 +306,7 @@ class LibContentContainer extends React.Component {
|
||||
isAllItemSelected={this.props.isAllDirentSelected}
|
||||
onAllItemSelected={this.props.onAllDirentSelected}
|
||||
selectedDirentList={this.props.selectedDirentList}
|
||||
onSelectedDirentListUpdate={this.props.onSelectedDirentListUpdate}
|
||||
onItemsMove={this.props.onItemsMove}
|
||||
onItemsCopy={this.props.onItemsCopy}
|
||||
onItemsDelete={this.props.onItemsDelete}
|
||||
|
@@ -1378,6 +1378,18 @@ class LibContentView extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
onSelectedDirentListUpdate = (newSelectedDirentList, lastSelectedIndex = null) => {
|
||||
this.setState({
|
||||
direntList: this.state.direntList.map(dirent => {
|
||||
dirent.isSelected = newSelectedDirentList.some(selectedDirent => selectedDirent.name === dirent.name);
|
||||
return dirent;
|
||||
}),
|
||||
isDirentSelected: newSelectedDirentList.length > 0,
|
||||
selectedDirentList: newSelectedDirentList,
|
||||
lastSelectedIndex: lastSelectedIndex,
|
||||
});
|
||||
};
|
||||
|
||||
onItemClick = (dirent) => {
|
||||
this.resetSelected();
|
||||
let repoID = this.props.repoID;
|
||||
@@ -2152,6 +2164,7 @@ class LibContentView extends React.Component {
|
||||
isDirentDetailShow={this.state.isDirentDetailShow}
|
||||
selectedDirent={this.state.selectedDirentList && this.state.selectedDirentList[0]}
|
||||
selectedDirentList={this.state.selectedDirentList}
|
||||
onSelectedDirentListUpdate={this.onSelectedDirentListUpdate}
|
||||
onItemsMove={this.onMoveItems}
|
||||
onItemsCopy={this.onCopyItems}
|
||||
onItemsDelete={this.onDeleteItems}
|
||||
|
Reference in New Issue
Block a user