1
0
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:
Aries
2024-07-26 17:15:18 +08:00
committed by GitHub
parent fa46b89b0d
commit 3a06447faf
9 changed files with 211 additions and 20 deletions

View File

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

View File

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

View File

@@ -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) => {

View File

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

View File

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

View File

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

View File

@@ -56,6 +56,7 @@
}
.dir-content-main {
position: relative;
flex: 1 0 74.5%;
display: flex;
flex-direction: column;

View File

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

View File

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