一、draggable+jquery实现
1、给所有的元素添加draggable=“true”属性
2、drop事件不能触发:在dragover事件中添加e.preventDefault()
目标效果:拖拽元素使之位于目标元素后面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>拖拽元素使之位于目标元素之后</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
*{
margin: 0;
padding: 0;
}
.container{
width: 100%;
height: 100%;
position: relative;
margin: auto;
font-size: 0;
}
.drag{
width: 100px;
height: 100px;
margin-left: 10px;
margin-bottom: 10px;
display: inline-block;
background-color: green;
opacity: 0.6;
vertical-align: top;
}
.example{
background-color: yellow;
}
.mask{
display: none;
background: white;
border: 1px dashed red;
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
}
.mousehover{
display: inline-block;
width: 10px;
height: 100px;
background-color: skyblue;
border-radius: 5px;
vertical-align: top;
}
</style>
<script>
// 记录当前被拖拽元素的大小和位置,以确定mask大小和位置
var width, height
var x = 0
var y = 0
// 位置提示条
var mousehover = '<div class="mousehover"></div>'
// 设置数组记录位置顺序
var sortArr = []
// 目标元素的索引
var currentIndex
// 目标元素的data-order
var currentDataOrder
$(function(){
drag.init('container')
})
var drag = {
class_name: null, // 允许放置的容器
_x: 0, // 节点x坐标
_y: 0, // 节点y坐标
_left: 0, // 光标和节点坐标的距离
_top: 0, // 光标和节点坐标的距离
old_elm: null, // 拖拽原节点
new_elm: null, // 拖拽完成后添加的新节点
// 初始化
init: function(className){
// 允许拖拽节点的父容器的className
drag.class_name = className
// 1.监听拖拽开始事件,动态绑定要拖拽的节点
$(document).on('dragstart', '.drag', function(e){
// 获取到拖拽的原节点对象
drag.old_elm = $(this)
// 执行拖拽开始事件的操作
drag.dragstartHandle(e)
// 专门定义一个函数可以在下面的on事件中获取到drag.old_elm
drag.getOldElm()
})
// 2.位置提示条的出现和消失
$(document).on('dragenter', '.drag', function(e){
$(e.target).after(mousehover)
$('.drag').off('dragenter')
})
$(document).on('dragover', '.drag', function(e){
e.preventDefault()
})
$(document).on('dragleave', '.drag', function(e){
$(e.target).nextAll('.mousehover').remove()
$('.drag').off('dragleave')
})
// 3.监听元素拖拽中事件
$(document).on('drop', '.drag', function(e){
// 删除位置提示条
$(e.target).nextAll('.mousehover').remove()
// 删除蒙版
$('.mask').remove()
// 获取到被拖拽元素
var old_elm = drag.getOldElm()
//删除被拖拽元素
$(old_elm).remove()
// 在目标元素后面添加被拖拽元素
$(e.target).after($(old_elm))
// 记录拖拽之后的顺序
if(sortArr.length !== 0){
sortArr.length = 0
}
$('.drag').each(function(index, item){
sortArr.push($(item).attr('data-order'))
})
})
},
dragstartHandle(e){
// 1. 设置蒙版的样式和出现位置
width = drag.old_elm.get(0).offsetWidth
height = drag.old_elm.get(0).offsetHeight
x = drag.old_elm.get(0).offsetLeft
y = drag.old_elm.get(0).offsetTop
$('.mask').css({
'width': width + 'px',
'height': height + 'px',
"left": x + 'px',
"top": y + 'px'
})
$('.mask').stop().show()
},
getOldElm(){
return drag.old_elm
}
}
</script>
</head>
<body>
<div class="container">
<div class="mask"></div>
<div class="drag" draggable="true" data-order="0">0</div>
<div class="drag" draggable="true" data-order="1">1</div>
<div class="drag" draggable="true" data-order="2">2</div>
<div class="drag example" draggable="true" data-order="3">3</div>
<div class="drag" draggable="true" data-order="4">4</div>
<div class="drag" draggable="true" data-order="5">5</div>
<div class="drag" draggable="true" data-order="6">6</div>
<div class="drag" draggable="true" data-order="7">7</div>
<div class="drag" draggable="true" data-order="8">8</div>
<div class="drag" draggable="true" data-order="9">9</div>
<div class="drag" draggable="true" data-order="10">10</div>
</div>
</body>
</html>
二、react通过mouseDown 、mousemove、mouseUp实现拖拽
当实现类似Excel选中区域的功能时,经常出现 mouseup 事件丢失的情况,由于缺少了 mouseup 事件,导致一个完整的操作无法进行。
目前发现两个原因:
- 触发了浏览器的 drag 操作,导致mouseup丢失。
- 由于鼠标离开了操作的区域,触发了mouseleave导致mouseup丢失。
- 最后的解决办法:1)mouseUp方法放到父元素上防止失去焦点。2)在mouseUp事件一开始就取消mousemove事件 document.mousemove = null。 3)取消冒泡和捕获事件。
// 阻止事件冒泡, 不仅仅要stopPropagation,还要preventDefault pauseEvent(e){ e=e || window.event; if(e.stopPropagation) e.stopPropagation(); if(e.preventDefault) e.preventDefault(); e.cancelBubble=true; e.returnValue=false; return false; }
解决办法
第一种情况
通过执行下面的代码阻止系统默认的操作来防止触发 drag 操作:
//在事件中
e=e || window.event;
pauseEvent(e);
//阻止事件冒泡
//不仅仅要stopPropagation,还要preventDefault
function pauseEvent(e){
if(e.stopPropagation) e.stopPropagation();
if(e.preventDefault) e.preventDefault();
e.cancelBubble=true;
e.returnValue=false;
return false;
}
通过对事件调用pauseEvent
方法可以防止出现drag操作,因此在区域内可以避免mouseup丢失。即使你想实现的本来就是 drag 操作,也可以通过创建跟随鼠标移动的dom元素实现效果。
参考地址:
http://stackoverflow.com/questions/5429827/how-can-i-prevent-text-element-selection-with-cursor-drag
第二种情况
由于鼠标移到了区域外,触发了 mouseleave 操作,因此在这种情况下要监听 mouseleave 操作,当触发该操作时可以停止或者还原状态。
特别注意的地方
当处理鼠标事件时,可以还要考虑是否要控制按下那个键时才允许操作。
Mouse事件中有一个 buttons
属性,该值标示鼠标按下了一个或者多个按键,如果按下的键为多个,值则为多个:
- 0 : 没有按键或者是没有初始化
- 1 : 鼠标左键
- 2 : 鼠标右键
- 4 : 鼠标滚轮或者是中键
- 8 : 第四按键 (通常是“浏览器后退”按键)
- 16 : 第五按键 (通常是“浏览器前进”)
多个值的时候,相当于进行|
操作,即鼠标左右键同时按下时1|2=3
。判断是否按下左键可以用value&1!=0
进行,例如左右键同时按下时3&1!=0
是true
,说明按下了左键。
子组件:
/*
@params title:移动块的标题
@params pendingNum:待处理数据的数量
@params clickPendingNum:点击待处理数量触发的函数
*/
import React, { Component } from 'react';
import './style.less'
export default class Drag extends Component {
constructor(props){
super(props)
this.state = {
// dragSize: {...props.options}.dragSize, // 移动块的大小类型,小:small、中:middle、大:great、宽:relax、巨:giant
sizeOptions: {...props.options}.sizeOptions, // 父组件传递过来的大小型号的选项,默认第一个是初始型号
allow_drag: false, // 是否允许拖拽
currentOrder: 0, // 被拖拽元素的data-order
targetOrder: -1, // 目标元素的data-order
moveOrder: 0, // 移动过程中途径元素的order,用于控制位置提示条的显隐
origin_left: 0, // 被拖拽元素原始相对于视图的X坐标
origin_top: 0, // 被拖拽元素原始相对于视图的Y坐标
mouse_left: 0, // 鼠标相对于视图的x坐标
mouse_top: 0, // 鼠标相对于视图的y坐标
fixed_left: 0, // 鼠标相对于元素的X坐标
fixed_top: 0, // 鼠标相对于元素的Y坐标
move_x: 0, // 元素相对于父元素的left
move_y: 0, // 元素相对于父元素的top
aPos: [], // 各个移动块的位置
}
this.mouseDown = this.mouseDown.bind(this)
this.mouseMove = this.mouseMove.bind(this)
this.mouseUp = this.mouseUp.bind(this)
this.findNearest = this.findNearest.bind(this)
this.getPosition = this.getPosition.bind(this)
this.pauseEvent = this.pauseEvent.bind(this)
}
// 开始拖拽事件
mouseDown(e){
console.log('鼠标按下事件')
this.pauseEvent(e);
// 鼠标按下时元素放大部分的宽度
var scaleWidth = e.target.closest('.move_block').offsetWidth * 0.05
if(!this.state.allow_drag){
this.setState({
currentOrder: parseInt(e.target.closest('.blockAndBg').dataset.order),
allow_drag: true,
origin_left: this.getPosition(e.target.closest('.blockAndBg')).left,
origin_top: this.getPosition(e.target.closest('.blockAndBg')).top,
fixed_left: e.pageX+document.getElementsByClassName('right-container')[0].scrollLeft - this.getPosition(e.target.closest('.blockAndBg')).left + scaleWidth / 2,
fixed_top: e.pageY+document.getElementsByClassName('right-container')[0].scrollTop - this.getPosition(e.target.closest('.blockAndBg')).top
},() => {
// 获取页面中所有移动块的位置
var arrPos = []
// 不能在遍历中改变aPos的状态,因为setState是异步更新的,改变的状态全都是最后的值
document.querySelectorAll('.blockAndBg').forEach((item, index) => {
arrPos.push(
{
left: item.offsetLeft,
right: item.offsetLeft+ item.offsetWidth,
top: item.offsetTop + document.getElementsByClassName('top')[0].offsetHeight,
bottom: item.offsetTop + document.getElementsByClassName('top')[0].offsetHeight + item.offsetHeight
}
)
})
this.setState({
aPos: arrPos
}, () => {
// 监听移动事件
document.onmousemove = (event) => {
this.mouseMove(event)
}
})
})
}
}
// 鼠标移动事件
mouseMove(e){
this.pauseEvent(e);
if(this.state.allow_drag){
// 获取鼠标移动的x,y坐标
this.setState({
mouse_left: e.pageX+document.getElementsByClassName('right-container')[0].scrollLeft,
mouse_top: e.pageY+document.getElementsByClassName('right-container')[0].scrollTop,
// 鼠标移动的x - (鼠标相对于元素在x轴上的距离 + blockAndBg的left值 ) = 元素absolute的left值
move_x: e.pageX+document.getElementsByClassName('right-container')[0].scrollLeft - this.state.fixed_left - this.state.origin_left,
move_y: e.pageY+document.getElementsByClassName('right-container')[0].scrollTop - this.state.fixed_top - this.state.origin_top
},()=>{
this.findNearest()
})
}
}
// 结束拖拽事件
mouseUp(e){
// 必须先删除mousemove事件,否则可能会无法实现鼠标抬起事件
document.onmousemove = null
this.pauseEvent()
this.setState({
allow_drag: false,
move_x: 0,
move_y: 0
},() => {
// 调用父元素的getTargetOrder方法传递给父元素拖拽对象的order和目标对象的order
this.props.options.getTargetOrder(this.state.currentOrder, this.state.targetOrder)
// 将位置提示条隐藏
this.props.options.changeMoveHover(-1)
})
}
findNearest(){
this.state.aPos.forEach((item, index) => {
if(this.state.mouse_left > item.left && this.state.mouse_left < item.right && this.state.mouse_top < item.bottom && this.state.mouse_top > item.top){
// 目标元素
var targetMove = document.querySelectorAll('.blockAndBg')[index]
var orderBy = parseInt(targetMove.dataset.order)
this.setState({
targetOrder: orderBy,
moveOrder: orderBy
},() => {
this.props.options.changeMoveHover(this.state.moveOrder)
})
}
})
}
// 获取元素到文档区域的坐标
getPosition(element){
if(element != null){
var rec = element.getBoundingClientRect(),
_x = rec.left, // 获取元素相对浏览器视窗window的左、上坐标
_y = rec.top;
// 与html或body元素的滚动距离相加就是元素相对于文档区域document的坐标位置
_x += document.getElementsByClassName('right-container')[0].scrollLeft;
_y += document.getElementsByClassName('right-container')[0].scrollTop;
return {
left: _x,
top: _y
};
}
}
// 阻止事件冒泡, 不仅仅要stopPropagation,还要preventDefault
pauseEvent(e){
e=e || window.event;
if(e.stopPropagation) e.stopPropagation();
if(e.preventDefault) e.preventDefault();
e.cancelBubble=true;
e.returnValue=false;
return false;
}
render() {
return (
<div className={`blockAndBg ${this.props.dragSize}`} data-order={this.props.options['data-order']}>
<div className={`bg ${!this.state.allow_drag ? "hide": ''}`}></div>
<div className="move_block list-divs-big" style={this.state.allow_drag ? {"zIndex": 999, transform:'scaleX(1.05)', opacity: 0.5, left: this.state.move_x, top: this.state.move_y} : null}>
<div className="list-div-top">
<span className="textcor" onClick={this.props.options.clickPendingNum}>
{this.props.options.title}
{this.props.options.pendingNum ? "(" : null}<i className='clientNum'>{this.props.options.pendingNum}</i>{this.props.options.pendingNum ? ")" : null}
</span>
<div className="right_bet" onMouseUp={e => this.mouseUp(e)}>
<span className="moveBtn" title="移动" onMouseDown={this.mouseDown}></span>
{this.props.options.moreClick ? <span className="moreClient" onClick={this.props.options.moreClick}>更多</span> : null}
<div className="install_border">
<div className="install_size"></div>
<div className="border_size visitors_size">
<div className={`small-line ${this.state.sizeOptions.indexOf("small") !== -1 ? 'show-flex' : 'hide'}`} onClick={() => {
this.props.options.getSizeFromOrder('small', this.props.options['data-order'])
}}>
<i className="iocsmall"></i>
<span>小</span>
</div>
<div className={`middle-line ${this.state.sizeOptions.indexOf("middle") !== -1 ? 'show-flex' : 'hide'}`} onClick={() => {
this.props.options.getSizeFromOrder('middle', this.props.options['data-order'])
}}>
<i className="iocmiddle"></i>
<span>中</span>
</div>
<div className={`great-line ${this.state.sizeOptions.indexOf("great") !== -1 ? 'show-flex' : 'hide'}`} onClick={() => {
this.props.options.getSizeFromOrder('great', this.props.options['data-order'])
}}>
<i className="iocgreat"></i>
<span>大</span>
</div>
<div className={`relax-line ${this.state.sizeOptions.indexOf("relax") !== -1 ? 'show-flex' : 'hide'}`} onClick={() => {
this.props.options.getSizeFromOrder('relax', this.props.options['data-order'])
}}>
<i className="iocrelax"></i>
<span>宽</span>
</div>
<div className={`giant-line ${this.state.sizeOptions.indexOf("giant") !== -1 ? 'show-flex' : 'hide'}`} onClick={() => {
this.props.options.getSizeFromOrder('giant', this.props.options['data-order'])
}}>
<i className="iocgiant"></i>
<span>巨</span>
</div>
</div>
</div>
</div>
</div>
<div>{this.props.children}</div>
</div>
{
this.props.showMoveHover === this.props.options['data-order'] && <div className="mousehover"></div>
}
</div>
);
}
}
父组件
import React from 'react';
import './style.less'
import Drag from '../../components/Drag'
import { useState, useEffect } from 'react';
export default function Overview() {
// 点击更多触发
const moreClick = () => {
console.log('tell me which num')
}
// 点击待处理触发
const clickPendingNum = () => {
console.log('overview 点击待处理数量')
}
// 改变位置提示条的显隐
const [moveHoverOrder, setMoveHoverOrder] = useState(-1)
const changeMoveHover = (value) => {
console.log(value)
setMoveHoverOrder(value)
}
// currentOrder: 被拖拽元素的data-order, targetOrder: 目标元素的data-order
const [sortArr, setSortArr] = useState([0,1,2,3])
var sort1 = sortArr.slice() // 浅复制数组
const getTargetOrder = (currentOrder, targetOrder) => {
if(targetOrder !== -1){
// 拖拽元素在sort1数组中的位置
var currentIndex = sort1.indexOf(currentOrder)
// 目标元素在sort1中的位置
var targetIndex = sort1.indexOf(targetOrder)
if(currentIndex < targetIndex){
// 拖拽元素在前,先增后减
sort1.splice(targetIndex+1, 0, currentOrder)
sort1.splice(currentIndex,1)
}else if(currentIndex > targetIndex){ // 目标元素在前,先减后增
sort1.splice(currentIndex,1)
sort1.splice(targetIndex+1, 0, currentOrder)
}
// 至关重要的一步,不能使用setSortArr(sort1),否则sortArr指向的内存地址不变,不会立即更新视图
setSortArr([...sort1])
}
}
// 获取到order对应的类型大小
const getSizeFromOrder = (size, order) => {
// console.log('size', size, 'order', order)
var optionsArr = [optionOne, optionTwo, optionThree, optionFour]
optionsArr.forEach((item) => {
if(item['data-order'] === order) {
// var changeItem = Object.assign({}, item, { dragSize: size })
var changeItem = {...item, dragSize: size}
if(item['data-order'] === 0){
setOptionOne(changeItem)
}else if(item['data-order'] === 1){
setOptionTwo(changeItem)
}else if(item['data-order'] === 2){
setOptionThree(changeItem)
}else if(item['data-order'] === 3){
setOptionFour(changeItem)
}
}
})
}
var [optionOne, setOptionOne] = useState({
sizeOptions: ['small', 'middle', 'great', 'relax', 'giant'],
dragSize: "small",
title:'第0个dragDiv',
"data-order": 0,
getTargetOrder: getTargetOrder,
changeMoveHover: changeMoveHover,
getSizeFromOrder: getSizeFromOrder,
childrenNode: () => {
return (
<>
<div style={{height: '100px',width: '100px', backgroundColor:'red'}}></div>
</>
)
}
})
var [optionTwo, setOptionTwo] = useState({
sizeOptions: ['small', 'middle', 'great', 'relax', 'giant'],
dragSize: "small",
title:'第1个dragDiv',
pendingNum: 2,
"data-order": 1,
clickPendingNum:clickPendingNum,
moreClick: moreClick,
getTargetOrder: getTargetOrder,
changeMoveHover: changeMoveHover,
getSizeFromOrder: getSizeFromOrder,
childrenNode: () => {
return (
<>
<div style={{height: '100px',width: '100px', backgroundColor:'orange'}}></div>
</>
)
}
})
var [optionThree, setOptionThree] = useState({
sizeOptions: ['small', 'middle', 'great', 'relax', 'giant'],
dragSize: "small",
title:'第2个dragDiv',
pendingNum: 3,
"data-order": 2,
clickPendingNum:clickPendingNum,
getTargetOrder: getTargetOrder,
changeMoveHover: changeMoveHover,
getSizeFromOrder: getSizeFromOrder,
childrenNode: () => {
return (
<>
<div style={{height: '100px',width: '100px', backgroundColor:'blue'}}></div>
</>
)
}
})
var [optionFour, setOptionFour] = useState({
sizeOptions: ['small', 'middle', 'great', 'relax', 'giant'],
dragSize: "small",
title:'第3个dragDiv',
pendingNum: 4,
"data-order": 3,
clickPendingNum:clickPendingNum,
getTargetOrder: getTargetOrder,
changeMoveHover: changeMoveHover,
getSizeFromOrder: getSizeFromOrder,
childrenNode: () => {
return (
<>
<div style={{height: '100px',width: '100px', backgroundColor:'green'}}></div>
</>
)
}
})
const [componentsArr, setComponentsArr] = useState([])
useEffect(() => {
var oComponentArr = []
sortArr.forEach((item1) => {
[optionOne, optionTwo, optionThree, optionFour].forEach((item2) => {
item2['data-order'] === item1 && oComponentArr.push(item2)
})
})
setComponentsArr(oComponentArr)
}, [sortArr, optionOne, optionTwo, optionThree, optionFour, moveHoverOrder]) // eslint-disable-line react-hooks/exhaustive-deps
return (
<div className="overview">
{
componentsArr.map((item) => {
return <Drag key={item["data-order"]} options={item} dragSize={item.dragSize} showMoveHover={moveHoverOrder}>{item.childrenNode()}</Drag>
})
}
</div>
);
}