拖拽释放算法
拖拽释放算法¶
统计信息:字数 10856 阅读22分钟
- 原理是什么
- 原生的JS怎么实现
- React-dnd 怎么实现
拖拽原理¶
- 设置某个DOM可拖拽,绑定拖拽事件的回调函数
- 设置某个DOM可释放,绑定释放事件的回调函数
- 开始拖拽,把相关的数据存放到拖动的对象上
- 拖动到可释放DOM上,释放,释放DOM的回调函数中获取数据,进行操作
样式
- 对于拖拽的DOM事件:拖拽开始;拖拽中;拖拽结束(不同的样式)
- 对于释放的DOM事件:可以释放;释放后(需要不同的样式)
React-dnd 基本实现¶
这是项目中 React-dnd 的使用方法,使用 2 版本。最新的 react-dnd 使用了 hook 和 typescript,使用 8 版本,所以下面的使用,和最新官方文档不同。
- 设置全部可拖拽可释放的DOM外层,使用 DragDropContext 包裹一层
- 把可以拖拽的部分用 DragSource 包裹
- 把可以释放的部分用 DropTarget 包裹
1 整体拖拽层
// 1 设置全部可拖拽可释放的DOM外层,使用 DragDropContext 包裹一层
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContext } from 'react-dnd';
class App { ... }
export default DragDropContext(HTML5Backend)(App);
2 可以拖拽的部分
// 2 把可以拖拽的部分用 DragSource 包裹
import { DragSource } from 'react-dnd';
class MyComponent { ... }
export default DragSource(type, spec, collect)(MyComponent);
3 可以释放的部分
// 3 把可以释放的部分用 DropTarget 包裹
import { DropTarget } from 'react-dnd';
class MyComponent2 { ... }
export default DropTarget(types, spec, collect)(MyComponent2);
原生的JS¶
这个参考目录树的代码,自己手写一次
tree-view.js
// 支持拖拽的树节点
class TreeView extends React.Component {
constructor(props) {
super(props);
this.state = {
isTreeViewDropTipShow: false,
};
// 根据用户的权限判断是否可以拖拽(读写权限,或者自定义权限满足)
const { userPerm } = props;
this.canDrop = userPerm === 'rw';
const { isCustomPermission, customPermission } = getUserPermission(userPerm);
if (isCustomPermission) {
const { modify } = customPermission.permission;
this.canDrop = modify;
}
}
// 移动项目的回调函数
onItemMove = (repo, dirent, selectedPath, currentPath) => {
this.props.onItemMove(repo, dirent, selectedPath, currentPath);
}
// 节点开始拖拽的函数(把数据存放在 dataTransfer 对象中)
onNodeDragStart = (e, node) => {
// 包括当前节点,当前节点路径,父节点的路径
let dragStartNodeData = {
nodeDirent: node.object,
nodeParentPath: node.parentNode.path,
nodeRootPath: node.path,
};
dragStartNodeData = JSON.stringify(dragStartNodeData);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('application/drag-item-info', dragStartNodeData);
}
// 当节点拖拽进入的回调函数
onNodeDragEnter = (e, node) => {
// 如果不支持拖拽,返回
if (!this.canDrop) {
return false;
}
e.persist();
// 如果支持拖拽,设置状态
if (e.target.className === 'tree-view tree ') {
this.setState({
isTreeViewDropTipShow: true,
});
}
}
// 节点拖拽移动(设置拖拽对象的属性是 move)
onNodeDragMove = (e) => {
if (!this.canDrop) {
return false;
}
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
// 当节点拖拽离开(改变状态)
onNodeDragLeave = (e, node) => {
if (!this.canDrop) {
return false;
}
if (e.target.className === 'tree-view tree tree-view-drop') {
this.setState({
isTreeViewDropTipShow: false,
});
}
}
// 当拖拽释放
onNodeDrop = (e, node) => {
if (!this.canDrop) {
return false;
}
// uploaded files
if (e.dataTransfer.files.length) {
return;
}
// 从拖拽对象中,获取节点属性,转换成对象
let dragStartNodeData = e.dataTransfer.getData('application/drag-item-info');
dragStartNodeData = JSON.parse(dragStartNodeData);
// 获取拖动的目录对象,父节点路径,子节点路径
let { nodeDirent, nodeParentPath, nodeRootPath } = dragStartNodeData;
let dropNodeData = node;
// 分别处理不同的拖拽情况
//move items 移动多个目录
if (Array.isArray(dragStartNodeData)) {
if (!dropNodeData) { //move items to root
if (dragStartNodeData[0].nodeParentPath === '/') {
this.setState({isTreeViewDropTipShow: false});
return;
}
this.props.onItemsMove(this.props.currentRepoInfo, '/');
this.setState({isTreeViewDropTipShow: false});
return;
}
this.onMoveItems(dragStartNodeData, dropNodeData, this.props.currentRepoInfo, dropNodeData.path);
return;
}
if (!dropNodeData) {
if (nodeParentPath === '/') {
this.setState({isTreeViewDropTipShow: false});
return;
}
this.onItemMove(this.props.currentRepoInfo, nodeDirent, '/', nodeParentPath);
this.setState({isTreeViewDropTipShow: false});
return;
}
if (dropNodeData.object.type !== 'dir') {
return;
}
if (nodeParentPath === dropNodeData.path) {
return;
}
// copy the dirent to itself. eg: A/B -> A/B
if (nodeParentPath === dropNodeData.parentNode.path) {
if (dropNodeData.object.name === nodeDirent.name) {
return;
}
}
// copy the dirent to it's child. eg: A/B -> A/B/C
if (dropNodeData.object.type === 'dir' && nodeDirent.type === 'dir') {
if (dropNodeData.parentNode.path !== nodeParentPath) {
let paths = Utils.getPaths(dropNodeData.path);
if (paths.includes(nodeRootPath)) {
return;
}
}
}
this.onItemMove(this.props.currentRepoInfo, nodeDirent, dropNodeData.path, nodeParentPath);
}
// 移动多个目录
onMoveItems = (dragStartNodeData, dropNodeData, destRepo, destDirentPath) => {
let direntPaths = [];
let paths = Utils.getPaths(destDirentPath);
dragStartNodeData.forEach(dirent => {
let path = dirent.nodeRootPath;
direntPaths.push(path);
});
if (dropNodeData.object.type !== 'dir') {
return;
}
// move dirents to one of them. eg: A/B, A/C -> A/B
if (direntPaths.some(direntPath => { return direntPath === destDirentPath;})) {
return;
}
// move dirents to current path
if (dragStartNodeData[0].nodeParentPath && dragStartNodeData[0].nodeParentPath === dropNodeData.path ) {
return;
}
// move dirents to one of their child. eg: A/B, A/D -> A/B/C
let isChildPath = direntPaths.some(direntPath => {
return paths.includes(direntPath);
});
if (isChildPath) {
return;
}
this.props.onItemsMove(destRepo, destDirentPath);
}
// 点击目录项,需要隐藏菜单
onMenuItemClick = (operation, node) => {
this.props.onMenuItemClick(operation, node);
hideMenu();
}
onMouseDown = (event) => {
event.stopPropagation();
if (event.button === 2) {
return;
}
}
onContextMenu = (event) => {
event.preventDefault();
let currentRepoInfo = this.props.currentRepoInfo;
if (currentRepoInfo.permission !== 'admin' && currentRepoInfo.permission !== 'rw') {
return '';
}
this.handleContextClick(event);
}
// 点击内容的回调函数
handleContextClick = (event, node) => {
event.preventDefault();
event.stopPropagation();
if (!this.props.isNodeMenuShow) {
return;
}
// 获取点击的位置
let x = event.clientX || (event.touches && event.touches[0].pageX);
let y = event.clientY || (event.touches && event.touches[0].pageY);
if (this.props.posX) {
x -= this.props.posX;
}
if (this.props.posY) {
y -= this.props.posY;
}
// 关闭当前的菜单
hideMenu();
// 根据点击的位置,显示新的菜单
let menuList = this.getMenuList(node);
let showMenuConfig = {
id: 'tree-node-contextmenu',
position: { x, y },
target: event.target,
currentObject: node,
menuList: menuList,
};
showMenu(showMenuConfig);
}
// 获取菜单项
getMenuList = (node) => {
let menuList = [];
let { NEW_FOLDER, NEW_FILE, COPY, MOVE, RENAME, DELETE, OPEN_VIA_CLIENT } = TextTranslation;
// 根据文件还是文件夹,显示不同的菜单
if (node.object.type === 'dir') {
menuList = [NEW_FOLDER, NEW_FILE, COPY, MOVE, RENAME, DELETE];
} else {
menuList = [RENAME, DELETE, COPY, MOVE, OPEN_VIA_CLIENT];
}
const { userPerm } = this.props;
const { isCustomPermission, customPermission } = Utils.getUserPermission(userPerm);
if (!isCustomPermission) {
return menuList;
}
// 根据不同的权限,增加不同的菜单等
menuList = [];
const { modify: canModify, delete: canDelete, copy: canCopy } = customPermission.permission;
if (!node) {
canModify && menuList.push(NEW_FOLDER, NEW_FILE);
return menuList;
}
if (node.object.type === 'dir') {
canModify && menuList.push(NEW_FOLDER, NEW_FILE);
}
canCopy && menuList.push(COPY);
canModify && menuList.push(MOVE, RENAME);
canDelete && menuList.push(DELETE);
if (node.object.type !== 'dir') {
menuList.push(OPEN_VIA_CLIENT);
}
return menuList;
}
render() {
return (
<div
className={`tree-view tree ${(this.state.isTreeViewDropTipShow && this.canDrop) ? 'tree-view-drop' : ''}`}
onDrop={this.onNodeDrop}
onDragEnter={this.onNodeDragEnter}
onDragLeave={this.onNodeDragLeave}
onMouseDown={this.onMouseDown}
onContextMenu={this.onContextMenu}
>
<TreeNodeView
userPerm={this.props.userPerm}
node={this.props.treeData.root}
currentPath={this.props.currentPath}
isNodeMenuShow={this.props.isNodeMenuShow}
isItemFreezed={this.state.isItemFreezed}
onNodeClick={this.props.onNodeClick}
onMenuItemClick={this.props.onMenuItemClick}
onNodeExpanded={this.props.onNodeExpanded}
onNodeCollapse={this.props.onNodeCollapse}
onNodeDragStart={this.onNodeDragStart}
onNodeDragMove={this.onNodeDragMove}
onNodeDrop={this.onNodeDrop}
onNodeDragEnter={this.onNodeDragEnter}
onNodeDragLeave={this.onNodeDragLeave}
handleContextClick={this.handleContextClick}
/>
</div>
);
}
}
TreeView.propTypes = propTypes;
export default TreeView;
Last update:
November 9, 2024