mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-20 19:08:21 +00:00
Feature/modify tag links by drag and drop (#7469)
* change tags link by drag and drop * optimize * optimize drag image * optimize drag effect * update codes --------- Co-authored-by: zhouwenxuan <aries@Mac.local> Co-authored-by: renjie-run <rj.aiyayao@gmail.com>
This commit is contained in:
@@ -158,6 +158,8 @@ SFTable.propTypes = {
|
||||
onGridKeyUp: PropTypes.func,
|
||||
loadMore: PropTypes.func,
|
||||
loadAll: PropTypes.func,
|
||||
moveRecords: PropTypes.func,
|
||||
renderCustomDraggedRows: PropTypes.func,
|
||||
};
|
||||
|
||||
export default SFTable;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { HorizontalScrollbar } from '../../scrollbar';
|
||||
import RecordsHeader from '../records-header';
|
||||
import Body from './body';
|
||||
@@ -7,6 +8,7 @@ import TreeBody from './tree-body';
|
||||
import GroupBody from './group-body';
|
||||
import RecordsFooter from '../records-footer';
|
||||
import ContextMenu from '../../context-menu';
|
||||
import RecordDragLayer from './record-drag-layer';
|
||||
import { RecordMetrics } from '../../utils/record-metrics';
|
||||
import { TreeMetrics } from '../../utils/tree-metrics';
|
||||
import { recalculate } from '../../utils/column';
|
||||
@@ -43,6 +45,7 @@ class Records extends Component {
|
||||
const { width: tableContentWidth } = props.getTableContentRect();
|
||||
const initHorizontalScrollState = this.getHorizontalScrollState({ gridWidth: tableContentWidth, columnMetrics, scrollLeft: 0 });
|
||||
this.state = {
|
||||
draggingRecordSource: null,
|
||||
columnMetrics,
|
||||
recordMetrics: this.createRowMetrics(),
|
||||
treeMetrics: this.createTreeMetrics(),
|
||||
@@ -832,13 +835,66 @@ class Records extends Component {
|
||||
return columnVisibleEnd;
|
||||
};
|
||||
|
||||
renderRecordsBody = ({ containerWidth }) => {
|
||||
const { recordMetrics, columnMetrics, colOverScanStartIdx, colOverScanEndIdx } = this.state;
|
||||
handleDragRecordsEnd = () => {
|
||||
this.setState({ draggingRecordSource: null });
|
||||
};
|
||||
|
||||
handleDragRecordStart = (event, { draggingRecordId, draggingTreeNodeKey }) => {
|
||||
this.setState({
|
||||
draggingRecordSource: {
|
||||
event,
|
||||
draggingRecordId,
|
||||
draggingTreeNodeKey
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleDropRecords = ({ dropRecordId, dropTreeNodeKey } = {}) => {
|
||||
const { showRecordAsTree } = this.props;
|
||||
const { draggingRecordSource, treeMetrics, recordMetrics } = this.state;
|
||||
const dropTarget = showRecordAsTree ? dropTreeNodeKey : dropRecordId;
|
||||
if (!dropTarget) return;
|
||||
|
||||
const { draggingRecordId, draggingTreeNodeKey } = draggingRecordSource;
|
||||
const draggingSource = showRecordAsTree ? TreeMetrics.getDraggedTreeNodesKeys(draggingTreeNodeKey, treeMetrics) : RecordMetrics.getDraggedRecordsIds(draggingRecordId, recordMetrics);
|
||||
this.props.moveRecords({ draggingSource, dropTarget });
|
||||
this.handleDragRecordsEnd();
|
||||
};
|
||||
|
||||
getRecordDragDropEvents = () => {
|
||||
if (!this.props.moveRecords) return null;
|
||||
if (!this.recordDragDropEvents) {
|
||||
this.recordDragDropEvents = {
|
||||
onDragStart: this.handleDragRecordStart,
|
||||
onDrop: this.handleDropRecords,
|
||||
onDragEnd: this.handleDragRecordsEnd,
|
||||
};
|
||||
}
|
||||
return this.recordDragDropEvents;
|
||||
};
|
||||
|
||||
createRecordsDragLayer = () => {
|
||||
const { draggingRecordSource, recordMetrics, treeMetrics } = this.state;
|
||||
if (!draggingRecordSource) return null;
|
||||
return (
|
||||
<RecordDragLayer
|
||||
showRecordAsTree={this.props.showRecordAsTree}
|
||||
draggingRecordSource={draggingRecordSource}
|
||||
recordMetrics={recordMetrics}
|
||||
treeMetrics={treeMetrics}
|
||||
renderCustomDraggedRows={this.props.renderCustomDraggedRows}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
renderRecordsBody = ({ containerWidth, recordDraggable }) => {
|
||||
const { recordMetrics, columnMetrics, colOverScanStartIdx, colOverScanEndIdx, draggingRecordSource } = this.state;
|
||||
const { columns, allColumns, totalWidth, lastFrozenColumnKey, frozenColumnsWidth } = columnMetrics;
|
||||
const recordDragDropEvents = this.getRecordDragDropEvents();
|
||||
const commonProps = {
|
||||
...this.props,
|
||||
columns, allColumns, totalWidth, lastFrozenColumnKey, frozenColumnsWidth,
|
||||
recordMetrics, colOverScanStartIdx, colOverScanEndIdx,
|
||||
recordMetrics, colOverScanStartIdx, colOverScanEndIdx, recordDraggable, recordDragDropEvents, draggingRecordSource,
|
||||
contextMenu: (
|
||||
<ContextMenu
|
||||
{...this.props}
|
||||
@@ -900,11 +956,12 @@ class Records extends Component {
|
||||
const containerWidth = this.getContainerWidth();
|
||||
const hasSelectedRecord = this.checkHasSelectedRecord();
|
||||
const isSelectedAll = this.checkIsSelectAll();
|
||||
const recordDraggable = !!this.props.moveRecords;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`sf-table-result-container ${this.isWindows ? 'windows-browser' : ''}`}
|
||||
className={classnames('sf-table-result-container', { 'windows-browser': this.isWindows, 'record-draggable': recordDraggable })}
|
||||
ref={this.setResultContainerRef}
|
||||
onScroll={this.onContentScroll}
|
||||
onClick={this.onClickContainer}
|
||||
@@ -933,9 +990,10 @@ class Records extends Component {
|
||||
modifyColumnWidth={this.props.modifyColumnWidth}
|
||||
insertColumn={this.props.insertColumn}
|
||||
/>
|
||||
{this.renderRecordsBody({ containerWidth })}
|
||||
{this.renderRecordsBody({ containerWidth, recordDraggable })}
|
||||
</div>
|
||||
</div>
|
||||
{this.createRecordsDragLayer()}
|
||||
{this.isWindows && this.isWebkit && (
|
||||
<HorizontalScrollbar
|
||||
ref={this.setHorizontalScrollbarRef}
|
||||
@@ -1010,6 +1068,7 @@ Records.propTypes = {
|
||||
getCopiedRecordsAndColumnsFromRange: PropTypes.func,
|
||||
moveRecord: PropTypes.func,
|
||||
addFolder: PropTypes.func,
|
||||
moveRecords: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Records;
|
||||
|
@@ -0,0 +1,36 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { TreeMetrics } from '../../utils/tree-metrics';
|
||||
import { RecordMetrics } from '../../utils/record-metrics';
|
||||
|
||||
const RecordDragLayer = ({ showRecordAsTree, draggingRecordSource, recordMetrics, treeMetrics, renderCustomDraggedRows }) => {
|
||||
const layerRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (layerRef.current && draggingRecordSource.event) {
|
||||
draggingRecordSource.event.dataTransfer.setDragImage(layerRef.current, 15, 15);
|
||||
}
|
||||
}, [draggingRecordSource]);
|
||||
|
||||
const getDraggedRowsIds = useCallback(() => {
|
||||
const { draggingRecordId, draggingTreeNodeKey } = draggingRecordSource;
|
||||
return showRecordAsTree ? TreeMetrics.getDraggedTreeNodesKeys(draggingTreeNodeKey, treeMetrics) : RecordMetrics.getDraggedRecordsIds(draggingRecordId, recordMetrics);
|
||||
}, [showRecordAsTree, draggingRecordSource, treeMetrics, recordMetrics]);
|
||||
|
||||
const renderDraggedRows = useCallback(() => {
|
||||
const draggedRowsIds = getDraggedRowsIds();
|
||||
if (renderCustomDraggedRows) {
|
||||
return renderCustomDraggedRows(draggedRowsIds);
|
||||
}
|
||||
return null;
|
||||
}, [getDraggedRowsIds, renderCustomDraggedRows]);
|
||||
|
||||
return (
|
||||
<div className="sf-table-record-drag-layer" ref={layerRef}>
|
||||
<table className='record-drag-layer-table'>
|
||||
<tbody>{renderDraggedRows()}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecordDragLayer;
|
@@ -70,6 +70,14 @@ class ActionsCell extends Component {
|
||||
onMouseEnter={this.onCellMouseEnter}
|
||||
onMouseLeave={this.onCellMouseLeave}
|
||||
>
|
||||
{this.props.recordDraggable &&
|
||||
<div
|
||||
draggable
|
||||
className="drag-handler"
|
||||
onDragStart={this.props.handleDragStart}
|
||||
>
|
||||
</div>
|
||||
}
|
||||
{!isSelected && <div className="sf-table-column-content row-index text-truncate">{this.getRecordNo()}</div>}
|
||||
<div className="sf-table-column-content actions-checkbox">
|
||||
<div className="select-cell-checkbox-container" onClick={this.props.onSelectRecord}>
|
||||
@@ -100,10 +108,12 @@ ActionsCell.propTypes = {
|
||||
isLocked: PropTypes.bool,
|
||||
isSelected: PropTypes.bool,
|
||||
isLastFrozenCell: PropTypes.bool,
|
||||
recordDraggable: PropTypes.bool,
|
||||
recordId: PropTypes.string,
|
||||
index: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
onSelectRecord: PropTypes.func,
|
||||
handleDragStart: PropTypes.func,
|
||||
};
|
||||
|
||||
export default ActionsCell;
|
||||
|
@@ -24,3 +24,62 @@
|
||||
.frozen-columns {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.sf-table-result-content .sf-table-row.can-drop-tip .sf-table-cell {
|
||||
background-color: rgb(200, 220, 240) !important;
|
||||
}
|
||||
|
||||
.sf-table-result-container.record-draggable .sf-table-row .drag-handler {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 1px;
|
||||
height: 30px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.sf-table-result-container.record-draggable .sf-table-row:hover .drag-handler {
|
||||
background-image: url(../../../../../../../media/img/grippy_large.png);
|
||||
background-repeat: no-repeat;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.sf-table-record-drag-layer {
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
left: -9999;
|
||||
top: 0;
|
||||
pointer-events: none;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.sf-table-record-drag-layer .record-drag-layer-table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.sf-table-record-drag-layer .rdg-dragged-record {
|
||||
display: flex;
|
||||
min-height: 32px;
|
||||
height: auto;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-right: 1px solid #ddd;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.sf-table-record-drag-layer .rdg-dragged-record:first-child {
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.sf-table-record-drag-layer .rdg-dragged-record .rdg-dragged-record-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 80px;
|
||||
height: 32px;
|
||||
padding: 8px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sf-table-record-drag-layer .rdg-dragged-record .rdg-dragged-record-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
@@ -10,11 +10,18 @@ import './index.css';
|
||||
|
||||
class Record extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
canDropTip: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.checkScroll();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return (
|
||||
nextProps.isGroupView !== this.props.isGroupView ||
|
||||
nextProps.hasSelectedCell !== this.props.hasSelectedCell ||
|
||||
@@ -40,7 +47,8 @@ class Record extends React.Component {
|
||||
nextProps.treeNodeKey !== this.props.treeNodeKey ||
|
||||
nextProps.treeNodeDepth !== this.props.treeNodeDepth ||
|
||||
nextProps.hasChildNodes !== this.props.hasChildNodes ||
|
||||
nextProps.isFoldedTreeNode !== this.props.isFoldedTreeNode
|
||||
nextProps.isFoldedTreeNode !== this.props.isFoldedTreeNode ||
|
||||
nextState.canDropTip !== this.state.canDropTip
|
||||
);
|
||||
}
|
||||
|
||||
@@ -222,23 +230,67 @@ class Record extends React.Component {
|
||||
return style;
|
||||
};
|
||||
|
||||
// handle drag copy
|
||||
handleDragStart = (event) => {
|
||||
event.stopPropagation();
|
||||
if (!this.props.recordDraggable) return;
|
||||
const { record, treeNodeKey } = this.props;
|
||||
const draggingRecordSource = { draggingRecordId: record._id, draggingTreeNodeKey: treeNodeKey };
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
this.props.recordDragDropEvents.onDragStart(event, draggingRecordSource);
|
||||
};
|
||||
|
||||
checkHasDraggedRecord = () => {
|
||||
return !!this.props.draggingRecordSource;
|
||||
};
|
||||
|
||||
checkOverDraggingRecord = () => {
|
||||
const { draggingRecordSource, record, treeNodeKey, showRecordAsTree } = this.props;
|
||||
if (!this.checkHasDraggedRecord()) return false;
|
||||
|
||||
const { draggingRecordId, draggingTreeNodeKey } = draggingRecordSource;
|
||||
if (showRecordAsTree) {
|
||||
return draggingTreeNodeKey === treeNodeKey;
|
||||
}
|
||||
return draggingRecordId === record._id;
|
||||
};
|
||||
|
||||
handleDragEnter = (e) => {
|
||||
// Prevent default to allow drop
|
||||
e.preventDefault();
|
||||
const { index, groupRecordIndex, cellMetaData: { onDragEnter } } = this.props;
|
||||
onDragEnter({ overRecordIdx: index, overGroupRecordIndex: groupRecordIndex });
|
||||
if (this.checkHasDraggedRecord() && !this.checkOverDraggingRecord()) {
|
||||
this.setState({ canDropTip: true });
|
||||
}
|
||||
};
|
||||
|
||||
handleDragLeave = (e) => {
|
||||
const { clientX, clientY } = e;
|
||||
const { left, top, width, height } = this.rowRef.getBoundingClientRect();
|
||||
if (clientX > left && clientX < left + width && clientY > top && clientY < top + height - 2) return;
|
||||
this.setState({ canDropTip: false });
|
||||
};
|
||||
|
||||
handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
e.dataTransfer.dropEffect = this.checkHasDraggedRecord() ? 'move' : 'copy';
|
||||
if (this.checkHasDraggedRecord() && !this.checkOverDraggingRecord()) {
|
||||
this.setState({ canDropTip: true });
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
e.stopPropagation();
|
||||
this.setState({ canDropTip: false });
|
||||
if (!this.checkHasDraggedRecord() || this.checkOverDraggingRecord()) {
|
||||
this.props.recordDragDropEvents.onDragEnd();
|
||||
return;
|
||||
}
|
||||
const { record, treeNodeKey } = this.props;
|
||||
const dropTarget = { dropRecordId: record._id, dropTreeNodeKey: treeNodeKey };
|
||||
this.props.recordDragDropEvents.onDrop(dropTarget);
|
||||
};
|
||||
|
||||
onDragEnd = () => {
|
||||
this.props.recordDragDropEvents.onDragEnd();
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -253,15 +305,19 @@ class Record extends React.Component {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rowRef => this.rowRef = rowRef}
|
||||
className={classnames('sf-table-row', {
|
||||
'sf-table-last-row': isLastRecord,
|
||||
'row-selected': isSelected,
|
||||
'row-locked': isLocked
|
||||
'row-locked': isLocked,
|
||||
'can-drop-tip': this.state.canDropTip,
|
||||
})}
|
||||
style={this.getRecordStyle()}
|
||||
onDragEnter={this.handleDragEnter}
|
||||
onDragLeave={this.handleDragLeave}
|
||||
onDragOver={this.handleDragOver}
|
||||
onDrop={this.handleDrop}
|
||||
onDragEnd={this.onDragEnd}
|
||||
>
|
||||
{/* frozen */}
|
||||
<div
|
||||
@@ -280,6 +336,8 @@ class Record extends React.Component {
|
||||
onSelectRecord={this.onSelectRecord}
|
||||
isLastFrozenCell={!lastFrozenColumnKey}
|
||||
height={cellHeight}
|
||||
recordDraggable={this.props.recordDraggable}
|
||||
handleDragStart={this.handleDragStart}
|
||||
/>
|
||||
}
|
||||
{frozenCells}
|
||||
@@ -311,6 +369,7 @@ Record.propTypes = {
|
||||
top: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
recordDraggable: PropTypes.bool,
|
||||
selectNoneCells: PropTypes.func,
|
||||
onSelectRecord: PropTypes.func,
|
||||
checkCanModifyRecord: PropTypes.func,
|
||||
|
@@ -598,6 +598,9 @@ class TreeBody extends Component {
|
||||
colOverScanStartIdx={this.props.colOverScanStartIdx}
|
||||
colOverScanEndIdx={this.props.colOverScanEndIdx}
|
||||
lastFrozenColumnKey={this.props.lastFrozenColumnKey}
|
||||
recordDraggable={this.props.recordDraggable}
|
||||
recordDragDropEvents={this.props.recordDragDropEvents}
|
||||
draggingRecordSource={this.props.draggingRecordSource}
|
||||
scrollLeft={scrollLeft}
|
||||
height={rowHeight}
|
||||
cellMetaData={cellMetaData}
|
||||
@@ -693,6 +696,9 @@ TreeBody.propTypes = {
|
||||
treeNodeKeyRecordIdMap: PropTypes.object,
|
||||
keyTreeNodeFoldedMap: PropTypes.object,
|
||||
treeMetrics: PropTypes.object,
|
||||
recordDraggable: PropTypes.bool,
|
||||
recordDragDropEvents: PropTypes.object,
|
||||
draggingRecordSource: PropTypes.object,
|
||||
columns: PropTypes.array.isRequired,
|
||||
CellOperationBtn: PropTypes.object,
|
||||
colOverScanStartIdx: PropTypes.number,
|
||||
@@ -727,7 +733,6 @@ TreeBody.propTypes = {
|
||||
frozenColumnsWidth: PropTypes.number,
|
||||
editMobileCell: PropTypes.func,
|
||||
reloadRecords: PropTypes.func,
|
||||
appPage: PropTypes.object,
|
||||
showCellColoring: PropTypes.bool,
|
||||
columnColors: PropTypes.object,
|
||||
onFillingDragRows: PropTypes.func,
|
||||
|
@@ -42,6 +42,14 @@ function isSelectedAll(recordIds, recordMetrics) {
|
||||
return recordIds.every(recordId => isRecordSelected(recordId, recordMetrics));
|
||||
}
|
||||
|
||||
function getDraggedRecordsIds(draggingRecordId, recordMetrics) {
|
||||
const selectedRecordIds = getSelectedIds(recordMetrics);
|
||||
if (selectedRecordIds.includes(draggingRecordId)) {
|
||||
return selectedRecordIds;
|
||||
}
|
||||
return [draggingRecordId];
|
||||
}
|
||||
|
||||
export const RecordMetrics = {
|
||||
selectRecord,
|
||||
selectRecordsById,
|
||||
@@ -51,4 +59,5 @@ export const RecordMetrics = {
|
||||
getSelectedIds,
|
||||
hasSelectedRecords,
|
||||
isSelectedAll,
|
||||
getDraggedRecordsIds,
|
||||
};
|
||||
|
@@ -49,6 +49,14 @@ const checkIsSelectedAll = (nodeKeys, treeMetrics) => {
|
||||
return nodeKeys.every(nodeKey => checkIsTreeNodeSelected(nodeKey, treeMetrics));
|
||||
};
|
||||
|
||||
const getDraggedTreeNodesKeys = (draggingTreeNodeKey, treeMetrics) => {
|
||||
const selectedNodeKeys = getSelectedTreeNodesKeys(treeMetrics);
|
||||
if (selectedNodeKeys.includes(draggingTreeNodeKey)) {
|
||||
return selectedNodeKeys;
|
||||
}
|
||||
return [draggingTreeNodeKey];
|
||||
};
|
||||
|
||||
export const TreeMetrics = {
|
||||
checkIsTreeNodeSelected,
|
||||
selectTreeNode,
|
||||
@@ -59,4 +67,5 @@ export const TreeMetrics = {
|
||||
getSelectedIds,
|
||||
checkHasSelectedTreeNodes,
|
||||
checkIsSelectedAll,
|
||||
getDraggedTreeNodesKeys,
|
||||
};
|
||||
|
Reference in New Issue
Block a user