# JavaScript 鼠标事件 和 HTML5 拖拽事件的综合应用

TIP

本节内容我们开始学习 JavaScript DOM 的鼠标事件,兼容性处理,各种滚动实践,鼠标按键,综合节流函数进行综合应用。

HTML5 拖拽事件,对象、属性、方法,兼容性处理,综合项目案例实践开发等

# 一、鼠标滚轮事件

TIP

鼠标滚轮事件存在兼容性问题,不同浏览器对应的事件名不一样。

具体有以下 3 种

事件名 事件对象属性(判断滚动方向) 支持浏览器
mousewheel(非标准) e.wheelDelta
正值(120)
负值(-120)
Webkit 和 Edge 等各大浏览器,但 Firefox 不支持
DOMMouseScroll(非标准) e.detail
正值(3)
负值(-3)
低版本 firefox,同时该事件需要通过 addEventListener 方来式绑定事件监听
wheel(标准) e.deltaY
正值
负值
各个厂商的高版本浏览器都支持

# 1、mousewheel 事件

TIP

  • 早期低版本的 webkit 和 Edge 支持
  • 通过 e.wheelDelta 的值来判断鼠标滚动的方向,值为 120 向上,值为-120 向下
box.addEventListener("wheel", eventFn, false);
function eventFn(e) {
  // 负值 向下滚  正值 向上滚
  console.log(e.wheelDelta);
}

# 2、DOMMouseScroll 事件

TIP

  • 低版本的 Firefox 支持
  • 通过 e.detail 的值来判断鼠标滚动方向,值为 3,向下滚,值为 -3 向上滚
box.addEventListener("DOMMouseScroll", eventFn, false);
function eventFn(e) {
  // 正值 3 向下滚  负值 -3  向上滚
  console.log(e.detail);
}

# 3、wheel 事件

TIP

  • 为了统一滚轮事件的标准,各大浏览器的高版本都支持这个事件
  • 通过 e.deltaY 的值来判断鼠标滚动方向,正值向下,负值向上。
var box = document.querySelector(".box");
box.addEventListener("wheel", eventFn, false);
function eventFn(e) {
  // 正值 向下滚  负值 向上滚
  console.log(e.deltaY);
}

# 4、兼容型处理

TIP

以下代码封装成wheel.js文件,供后期使用

/**
 * addWheelListener 兼容不同版板的鼠标滚轮事件
 * element 绑定滚轮事件的元素
 * eventFn 事件处理函数
 * useCapture 在捕获还是冒泡阶段执行
 */
window.addWheelListener = function (element, eventFn, useCapture = false) {
  // 获取浏览器当前支持的版本的鼠标滚轮事件名
  var support =
    "onwheel" in document.createElement("div")
      ? "wheel" // 各个厂商的高版本浏览器都支持"wheel"
      : document.onmousewheel !== undefined
      ? "mousewheel" // Webkit 和 IE 一定支持"mousewheel"
      : "DOMMouseScroll"; // 低版本 firefox

  // 添加鼠标事件
  element.addEventListener(support, callback, useCapture);

  // 兼容写法的事件方法
  function callback(e) {
    var e = e || window.event;
    e.preventDefault(); // 阻止默认行为
    // 处理低版本火狐
    e.detail && (e.deltaY = e.detail * 40);
    // 处理低版本 ie 和  Edge等浏览器
    e.wheelDelta && (e.deltaY = -e.wheelDelta);

    // 滚动事件要处理的事情,通过e.deltaY来判断滚动的方向,正向下,负向上
    eventFn.apply(this, arguments);
  }
};

应用

<style>
  .box {
    width: 200px;
    height: 200px;
    background-color: khaki;
    margin: 100px;
  }
</style>

<div class="box"></div>
<script>
  var box = document.querySelector(".box");
  // 添加鼠标滚轮事件
  addWheelListener(box, fn, false);
  // 事件处理函数
  function fn(e) {
    console.log("e.deltaY" + e.deltaY);
    console.log(this);
  }
</script>

# 5、案例:滚动实现元素高度变化

GIF2022-11-280-36-53

<style>
  .box {
    width: 100px;
    height: 100px;
    background-color: khaki;
    margin: 100px;
  }
</style>

<div class="box"></div>
<script src="./wheel.js"></script>
<script>
  var box = document.querySelector(".box");
  // 添加滚轮事件(封装后的方法)
  addWheelListener(box, fn, false);
  function fn(e) {
    var deltaY = e.deltaY;
    var speed = 10;
    if (deltaY > 0) {
      // 向下滚
      var height = this.offsetHeight + speed;
      if (height > 200) height = 200;
      this.style.height = height + "px";
    } else {
      // 向上滚
      var height = this.offsetHeight - speed;
      if (height < 50) height = 50;
      this.style.height = height + "px";
    }
  }
</script>

# 6、案例:全屏垂直滚动轮播

TIP

涉及知识点

  • 鼠标滚轮事件
  • transitionend 事件
  • 节流操作
  • 事件委托
  • window.resize 事件

GIF2022-11-2723-16-06

# 6.1、CSS 布局代码

<style>
  html,
  body {
    height: 100%;
    margin: 0;
    padding: 0;
    overflow: hidden;
  }
  .wrap {
    width: 100%;
    height: 500%;
    transform: translate3d(0, 0, 0);
    transition: transform 1s ease;
    cursor: pointer;
  }
  .wrap .slide {
    width: 100%;
    height: 20%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 80px;
    color: #fff;
  }
  .wrap .slide:nth-child(1) {
    background-color: rgb(248, 224, 179);
  }
  .wrap .slide:nth-child(2) {
    background-color: rgb(179, 248, 191);
  }
  .wrap .slide:nth-child(3) {
    background-color: rgb(243, 189, 201);
  }
  .wrap .slide:nth-child(4) {
    background-color: rgb(208, 207, 248);
  }
  .wrap .slide:nth-child(5) {
    background-color: rgb(247, 207, 204);
  }

  .pagination {
    position: fixed;
    width: 10px;
    height: 120px;
    right: 20px;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    flex-direction: column;
    justify-content: space-between;
  }
  .pagination span {
    width: 10px;
    height: 10px;
    background-color: #fff;
    border-radius: 50%;
    cursor: pointer;
  }
  .pagination span.active {
    background-color: orange;
    box-shadow: 0 0 2px 5px orange;
  }
</style>

<!-- 滚动容器 -->
<div class="wrap">
  <div class="slide">第一屏</div>
  <div class="slide">第二屏</div>
  <div class="slide">第三屏</div>
  <div class="slide">第四屏</div>
  <div class="slide">第五屏</div>
</div>

<!-- 分页器 -->
<div class="pagination">
  <span class="active"></span>
  <span></span>
  <span></span>
  <span></span>
  <span></span>
</div>

# 6.2、利用 transitionend 事件,来实现节流

<script src="./wheel.js"></script>
<script>
  // 获取所有的spans
  var spans = document.querySelectorAll(".pagination span");
  var pagination = document.querySelector(".pagination");
  var viewHeight = document.documentElement.clientHeight;
  var wrap = document.querySelector(".wrap");
  var currentindex = 0; // 保存当前的序号
  var prevIndex = 0; // 保存前一项序号
  var len = spans.length; // 按扭个数
  var lock = false; // false表示当前没有锁

  // 给每个元素身上添加自定义属性index,用来记录当前元素身上的序号
  for (var i = 0; i < spans.length; i++) {
    spans[i].index = i; // 给每个span标签添加一个自定义属性index,用来保存对应的序号
  }

  to(2); // 刚开始显示那个页面
  // 把span元素的点击事件要处理的事情,委托给他的父亲
  pagination.onclick = fn;
  function fn(e) {
    var target = e.target;
    var tagName = target.tagName.toLowerCase();
    if (tagName !== "span") return;
    if (lock) return;
    lock = true;
    // 如果是重复点一个span,那就要考虑解锁
    if (currentindex === prevIndex) {
      lock = false;
    }
    // 需要处理事情
    // 清除前一个被点击元素的样式
    spans[prevIndex].classList.remove("active");
    currentindex = target.index;
    prevIndex = currentindex;
    spans[currentindex].classList.add("active");

    var translateY = -currentindex * viewHeight;
    wrap.style.transform = "translate3d(0, " + translateY + "px, 0)";
  }

  // 处理滚轮事件
  addWheelListener(wrap, wheelFn); // 做了节流操作
  function wheelFn(e) {
    if (e.deltaY > 0) {
      // 向下滚
      if (lock) return;
      lock = true;
      currentindex++; // 4
      if (currentindex === len) {
        currentindex = len - 1;
        lock = false; // 开锁
        return;
      }
      // 滚动到指定下标元素
      to(currentindex);
    } else {
      // 向上滚
      if (lock) return;
      lock = true;
      currentindex--;
      if (currentindex < 0) {
        currentindex = 0;
        lock = false; // 开锁
        return;
      }
      // 滚动到指定下标元素
      to(currentindex);
    }
  }

  // 跳转到指定的下标元素
  function to(index) {
    currentindex = index; // 保存当前下标
    spans[prevIndex].classList.remove("active");
    prevIndex = currentindex;
    spans[currentindex].classList.add("active");
    var translateY = -currentindex * viewHeight;
    wrap.style.transform = "translate3d(0, " + translateY + "px, 0)";
  }

  // 监听wrap身上的 transitionend 事件
  wrap.addEventListener("transitionend", function () {
    lock = false; // 解锁
  });

  // 当浏览器的高度发生改变时,那就需要重新计算veiwHeight的高度
  window.addEventListener("resize", function () {
    // 同时还要重新计算,wrap 的translateY的值
    viewHeight = document.documentElement.clientHeight;
    var translateY = -(currentindex * viewHeight);
    wrap.style.transform = "translate3d(0, " + translateY + "px, 0)";
  });
</script>

# 6.3、利用节流函数,来实现节流

<script src="./wheel.js"></script>
<script>
  // 获取所有的spans
  var spans = document.querySelectorAll(".pagination span");
  var pagination = document.querySelector(".pagination");
  var viewHeight = document.documentElement.clientHeight;
  var wrap = document.querySelector(".wrap");
  var currentindex = 0; // 保存当前的序号
  var prevIndex = 0; // 保存前一项序号
  var len = spans.length; // 按扭个数

  // 给每个元素身上添加自定义属性index,用来记录当前元素身上的序号
  for (var i = 0; i < spans.length; i++) {
    spans[i].index = i; // 给每个span标签添加一个自定义属性index,用来保存对应的序号
  }
  // 把span元素的点击事件要处理的事情,委托给他的父亲
  pagination.onclick = throttle(fn, 1000);
  function fn(e) {
    var target = e.target;
    var tagName = target.tagName.toLowerCase();
    if (tagName !== "span") return;
    // 需要处理事情
    // 清除前一个被点击元素的样式
    spans[prevIndex].classList.remove("active");
    currentindex = target.index; // 更新当前下标
    prevIndex = currentindex; // 更新前一个下标
    target.classList.add("active"); // 给当前元素添加样式
    // 滚动的距离
    var translateY = -currentindex * viewHeight;
    wrap.style.transform = "translate3d(0, " + translateY + "px, 0)";
  }

  // 处理滚轮事件
  addWheelListener(wrap, throttle(wheelFn, 1000)); // 做了节流操作
  function wheelFn(e) {
    if (e.deltaY > 0) {
      // 向下滚
      currentindex++;
      if (currentindex === len) {
        currentindex = len - 1;
        return;
      }
      spans[prevIndex].classList.remove("active");
      prevIndex = currentindex;
      spans[currentindex].classList.add("active");
      var translateY = -currentindex * viewHeight;
      wrap.style.transform = "translate3d(0, " + translateY + "px, 0)";
    } else {
      // 向上滚
      currentindex--;
      if (currentindex < 0) {
        currentindex = 0;
        return;
      }
      spans[prevIndex].classList.remove("active"); // 移除前一项样式
      prevIndex = currentindex;
      spans[currentindex].classList.add("active"); // 当前项添加样式
      var translateY = -currentindex * viewHeight; // 滚动的距离
      wrap.style.transform = "translate3d(0, " + translateY + "px, 0)";
    }
  }

  // 当浏览器的高度发生改变时,那就需要重新计算veiwHeight的高度
  window.addEventListener("resize", function () {
    // 同时还要重新计算,wrap 的translateY的值
    viewHeight = document.documentElement.clientHeight;
    var translateY = -(currentindex * viewHeight);
    wrap.style.transform = "translate3d(0, " + translateY + "px, 0)";
  });

  // 节流函数
  function throttle(fn, delay = 20) {
    var timer = null; // null表示当前锁是打开的,没有锁,可以执行事件处理函数中的代码
    return function () {
      if (timer) return;
      var self = this; // 保存this 绑定事件的对象
      var args = arguments; // 保存arguments 主要用来获取事件对象 e
      // 定时器计时,用来开锁
      timer = setTimeout(function () {
        timer = null; // 开锁
      }, delay);

      fn.apply(self, args); // 事件处理函数
    };
  }
</script>

# 二、鼠标按键

TIP

深入浅出鼠标按键,禁止右键菜单、禁止选中元素、判断按下的鼠标键、自定义右键菜单等实际开发中常用的方法及应用场景。

# 1、禁止右键菜单

TIP

  • contextmenu事件,会在鼠标点击右键或者按下键盘上的菜单键时被触发,用于显示上下文菜单
  • 如果我们希望在按下右键时,不要显示上下文菜单,我们可以在contextmenu事件中,阻止默认行为。
// 在页面任意位置按下右键,都不会显示上下文菜单
document.addEventListener("contextmenu", function (e) {
  e.preventDefault();
});

var box = document.querySelector(".box");
// 只在当前元素上按下时,不显示上下文菜单
box.addEventListener("contextmenu", function (e) {
  e.preventDefault();
});

# 2、禁止选中元素

TIP

selectstart 事件,在用户开始一个新的选择时候触发

如果不想用户选择内容,可以在事件中阻止默认行为

document.onselectstart = function (e) {
  e.preventDefault();
};

# 3、判断按下的鼠标键

TIP

  • MouseEvent.button是只读属性,它返回一个值,代表用户按下并触发了事件的鼠标按键。
  • 常用于在 onmouseup 事件中,判断用户按下的是鼠标左键 、中键、右键中的那个键
属性值 说明
0 主按键,通常指鼠标左键或默认值
1 辅助按键,通常指鼠标滚轮中键
2 次按键,通常指鼠标右键
<style>
  .box {
    width: 200px;
    height: 200px;
    background-color: khaki;
  }
</style>

<div class="box"></div>
<script>
  var box = document.querySelector(".box");
  box.onmouseup = function (e) {
    console.log(e.button); // 0 左键  1中键  2右键
  };
</script>

# 4、自定义右键菜单

TIP

涉及知识点

  • 禁止右键菜单
  • 事件委托
  • e.button 判断按下的鼠标键

GIF2022-11-280-35-02

<style>
  html,
  body,
  ul,
  li {
    margin: 0;
    padding: 0;
  }
  li {
    list-style: none;
  }
  .menu {
    width: 100px;
    border: 1px solid #ddd;
    position: absolute;
    left: 100px;
    top: 200px;
    box-shadow: 2px 2px 3px #ddd;
    display: none;
  }
  .menu li {
    height: 30px;
    line-height: 30px;
    text-indent: 1em;
    border-bottom: 1px solid #ddd;
    font-size: 12px;
    cursor: pointer;
  }
  .menu li:hover {
    background-color: #ddd;
  }
  .skin {
    width: 600px;
    height: 300px;
    background-color: khaki;

    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    display: none;
  }
  .skin .close {
    width: 40px;
    height: 40px;
    background-color: #000;
    text-align: center;
    line-height: 40px;
    position: absolute;
    right: -20px;
    top: -20px;
    border-radius: 50%;
    color: #fff;
    font-weight: bold;
    cursor: pointer;
  }
</style>
<body>
  <ul class="menu">
    <li>更换背景图</li>
    <li>新建标签</li>
    <li>显示隐藏元素</li>
    <li>其它操作</li>
  </ul>

  <div class="skin">
    更换皮肤
    <div class="close">X</div>
  </div>

  <script>
    var menu = document.querySelector(".menu");
    var list = document.querySelectorAll(".menu li");
    var skin = document.querySelector(".skin");
    var close = document.querySelector(".skin .close");
    // 禁止右键菜单
    document.oncontextmenu = function (e) {
      e.preventDefault();
    };
    // 鼠标按下时
    document.onmouseup = function (e) {
      if (e.button === 2) {
        // 表示按下了右键,显示自定义的右菜单,同时右菜单在鼠标的右下角显示
        menu.style.left = e.pageX + "px";
        menu.style.top = e.pageY + "px";
        menu.style.display = "block";
      }
    };
    // 在页面任意位置点击,即关闭自定义菜单
    document.onclick = function () {
      menu.style.display = "none";
    };

    // 为菜单选项,添加点击事件,点击后做相关操作,事件委托
    for (var i = 0; i < list.length; i++) {
      list[i].index = i;
    }
    // 事件委托,右键菜单的点击事件委托给父元素来处理
    menu.onclick = function (e) {
      var target = e.target;
      var tagName = target.tagName.toLowerCase();
      if (tagName !== "li") return;
      var index = target.index;
      switch (index) {
        case 0:
          skin.style.display = "block"; // 显示换肤界面
          break;
        case 1:
          break;
        case 2:
          break;
        case 3:
          break;
      }
    };
    // 点击关闭按扭,关闭换肤
    close.onclick = function () {
      skin.style.display = "none";
    };
  </script>
</body>

# 三、HTML5 的拖拽事件

TIP

  • 默认情况下,网页中只有图片和文本可以拖动。其它元素默认情况下,均不允许被拖动
  • 在图片上按下鼠标不放,然后移动鼠标就可以拖动图片。
  • 当需要拖动文本时,只需要先选中文本,然后在选中文本上按下,最后移动鼠标,就可以实现拖动。
<style>
  .box {
    width: 100px;
    height: 100px;
    background-color: khaki;
  }
</style>

我是文字,选中就能拖
<div class="box"></div>
<img src="https://www.arryblog.com/logo.png" alt="" width="200px" />
<a href="">进入艾编程官网</a>

GIF2022-11-2513-37-49

如果想要其它元素也能被拖动呢 ?

  • 在 HTML5 中为每个元素提供了一个draggable属性,这个属性用于标识元素是否允许使用拖放操作拖动。
  • 我们可以在元素身上添加draggabel属性,同时值为字符"true",元素就可以被拖动了

# 1、draggable 属性

TIP

draggable 属性有以下三个值,用来控制元素是否能被拖拽

属性值 描述
true 表示元素可以被拖动
false 表示元素不可以被拖动
auto 默认值,表示使用浏览器定义的默认行为
默认情况下,只有已选中的文本、图片、链接可以拖动,其它元素是不可拖拽的。
<style>
  .box {
    width: 100px;
    height: 100px;
    background-color: khaki;
  }
</style>
<div class="box" draggable="true">我是文字,但我不能被选中</div>

GIF2022-11-2816-32-42

注:

当元素被设置为可拖拽时,其元素中的文本和其子元素都不能以正常的方式被选中

# 2、拖拽基础概念

TIP

  • 在拖放(drag and drop)操作过程中,分为拖拽元素目标元素(可放置元素)
  • 拖拽元素: 鼠标按下进行拖拽的元素为拖拽元素
  • 目标元素: 把拖拽元素放入某个元素内部,或与某个元素发生碰撞等,这些元素为目标元素(可放置元素)。

如下图:假设把 A 元素拖拽放到 B 元素中,那A元素为拖拽元素,B元素为目标元素(可放置元素)

image-20221125135402987

为了更方便实现 HTML 拖放(Drag and Drop)操作,我们来学习下拖拽相关的事件。

# 3、拖拽事件

TIP

元素在拖拽期间,会触发一些事件类型,其中分为

  • 拖拽元素事件:dragstartdragdragend
  • 目标元素(放置元素)事件:dragenterdragoverdragleavedrop

# 3.1、拖拽元素事件:drag、dragstart 、dragend

事件名 说明
dragstart 当按住鼠标键不放并开始移动鼠标的那一刻,被拖动元素上会触发dragstart事件。只会在刚开始移动时触发一次。
drag 只要拖拽元素在拖拽中,就会持续的触发drag事件,有点类似于 mousemove 事件,只要鼠标在移动就会不断触发
dragend dragend 事件在拖放操作结束时触发(通过释放鼠标按钮或单击 escape 键),只会在结束时触发一次
  • 通常会在dragstart中,设置被拖拽元素为半透明,标识元素正在被拖动。
  • dragend中,恢复被拖拽元素为不透明。
<style>
  .drag {
    width: 100px;
    height: 100px;
    background-color: khaki;
    position: absolute;
    left: 100px;
    top: 100px;
  }
</style>

<!-- draggable=true 表示元素可拖拽 -->
<div class="drag" draggable="true"></div>
<script>
  var drag = document.querySelector(".drag");

  // 按下,刚开始拖拽事
  drag.ondragstart = function () {
    this.style.opacity = 0.3; // 设置被拖拽元素的透明度
    console.log("dragstart事件");
  };
  // 拖拽过程中一直会触发
  drag.ondrag = function () {
    console.log("drag事件");
  };
  // 鼠标松开,不拖拽事触发
  drag.ondragend = function () {
    this.style.opacity = 1; // 设置被拖拽元素的透明度
    console.log("dragend事件");
  };
</script>

image-20221123150253835

温馨提示:

  • dragstart 事件类传于mousedown事件,不过是在按下后开始拖拽才触发,在 drag事件前触发
  • drag事件类似于mousemove事件,会在拖动过程中频繁的触发
  • dragend事件类似于 mouseup事件,会在鼠标弹起时(不拖拽)时触发

所有子元素的拖拽事件,都会冒泡到他们的父元素身上,所以在处理拖拽事件时,可以利用事件委托

<style>
  .box {
    width: 200px;
    height: 200px;
    background-color: khaki;
  }
  .box2 {
    width: 100px;
    height: 100px;
    background-color: skyblue;
  }
</style>

<div class="box">
  <div class="box2" draggable="true"></div>
</div>
<script>
  var box = document.querySelector(".box");
  var box2 = document.querySelector(".box2");

  // 给box2添加对应拖拽事件
  box2.ondragstart = function (e) {
    console.log("box2-start");
  };
  box2.ondrag = function (e) {
    console.log("box2-drag");
  };
  box2.ondragend = function (e) {
    console.log("box2-end");
  };

  // 以下元素默认不能拖,但事件能被成功触发
  // 是因为box2元素上对应事件触发后,会冒泡到父元素上
  box.ondragstart = function (e) {
    console.log("box-start");
  };
  box.ondrag = function (e) {
    console.log("box-drag");
  };
  box.ondragend = function (e) {
    console.log("box-end");
  };

  document.ondragstart = function (e) {
    console.log("document-start");
  };
  document.ondrag = function (e) {
    console.log("document-drag");
  };
  document.ondragend = function (e) {
    console.log("document-end");
  };
</script>

image-20221130150423150

# 3.2、dragenter、dragover、dragleave 放置元素事件

事件名 说明
dragenter 当拖动的元素或被选择的文本进入有效的放置目标时, dragenter 事件被触发。
dragover 当元素或者选择的文本被拖拽到一个有效的放置目标上时,触发 dragover 事件(每几百毫秒触发一次)
dragleave dragleave 事件在拖动的元素或选中的文本离开一个有效的放置目标时被触发
<style>
  .drag {
    width: 100px;
    height: 100px;
    position: absolute;
    left: 100px;
    top: 100px;
    background-color: khaki;
    z-index: 3;
  }
  .target {
    width: 200px;
    height: 200px;
    background-color: skyblue;
    position: absolute;
    left: 100px;
    top: 300px;
  }
</style>

<div class="drag" draggable="true"></div>
<div class="target"></div>

<script>
  // 拖拽元素
  var drag = document.querySelector(".drag");
  // 目标元素
  var target = document.querySelector(".target");

  // 当拖拽元素进入目标元素时触发
  target.ondragenter = function () {
    console.log("dragenter");
  };

  // 当拖拽元素在有效的目标位置上时会触发
  target.ondragover = function () {
    console.log("dragover");
  };

  // 当拖拽元素离开有效的目标位置上时会触发
  target.ondragleave = function () {
    console.log("dragleave");
  };
</script>

GIF2022-11-2817-22-06

# 3.3、drop 事件

事件名 说明
drop drop 事件在被拖拽元素或选中的文本被放置在有效的放置目标上时被触发

# 3.4、默认情况,元素不允许放置

TIP

  • 在网页中,默认情况下,唯一有效的放置目标元素是文本框。但默认只能放置文本、链接、图片
  • 页面其它元素默认情况下,是不允许放置。如果把元素拖动到不允许放置的目标上,然后放下,不会触发 drop 事件。
<style>
  .drag {
    width: 100px;
    height: 100px;
    background-color: khaki;
  }
  .target {
    width: 200px;
    height: 100px;
    border: 2px dashed #ddd;
    position: absolute;
    top: 10px;
    left: 300px;
  }
  textarea {
    width: 200px;
    height: 100px;
    position: absolute;
    left: 300px;
    top: 150px;
  }
</style>

<img src="https://www.arryblog.com/logo.png" width="100" />
我是一段文字
<a href="www.baidu.com">百度</a>
<div class="drag" draggable="true"></div>
<textarea name="" id="text" cols="30" rows="10"></textarea>
<div class="target"></div>
<script>
  var drag = document.querySelector(".drag");
  var text = document.getElementById("text");
  var target = document.querySelector(".target");

  text.ondrop = function () {
    console.log("text - drop");
  };
  target.ondrop = function () {
    console.log("target - drop ");
  };
</script>

GIF2022-11-2817-39-24

# 3.5、如何使放置元素触发 drop 事件

TIP

  • 如果被拖拽元素在进入放置(目标)元素时,其鼠标状态显示禁止,则表示禁止该行为,在鼠标松开时,不会触发drop事件。
  • 因为默认情况,元素是不允许放置的,要使目标元素能够接收到drop事件,需要在dragenterdragover 事件中阻止默认行为。
<style>
  .drag {
    width: 100px;
    height: 100px;
    background-color: khaki;
  }
  .target {
    width: 200px;
    height: 200px;
    border: 2px dashed #ddd;
    position: absolute;
    top: 10px;
    left: 120px;
  }
</style>

<div class="drag" draggable="true"></div>
<div class="target"></div>
<script>
  var drag = document.querySelector(".drag");
  var target = document.querySelector(".target");
  // 要使放置元素的drop事件能触发,需要在dragover中禁止其默认行为
  target.ondragover = function (e) {
    e.preventDefault();
  };
  // 放置元素绑定drop事件
  target.ondrop = function () {
    console.log("drop");
  };
</script>

# 3.6、drop 事件兼容问题

TIP

在 Firefox 浏览器中,drop 放置事件的默认行为如下:

  • 如果拖动元素是链接,则在放置(目标)元素上松开鼠标时,会导航到对应页面
  • 如果拖动元素是图片,则在放置(目标)元素上松开鼠标标时,会导航到图片文件
  • 如果拖动元素是文本,会导致无效的 URL 错误(或默认开启百度搜索)

所以如果拖动元素为以上三者,我们需要在 drop 放置事件中,阻止其默认行为事件冒泡

e.preventDefault(); // 阻止默认行为
e.stopPropagation(); // 阻止事件冒泡
<style>
  .drag {
    width: 100px;
    height: 100px;
    background-color: khaki;
  }
  .target {
    width: 200px;
    height: 200px;
    border: 2px dashed #ddd;
    position: absolute;
    top: 10px;
    left: 220px;
  }
</style>

<a href="http://www.baidu.com">我是大美人</a><br />
<img src="https://www.arryblog.com/logo.png" alt="" width="100" /><br />
我只是个文字
<div class="drag" draggable="true"></div>
<div class="target"></div>
<script>
  var drag = document.querySelector(".drag");
  var target = document.querySelector(".target");
  // 要使放置元还给的drop事件能触发,需要在dragover中禁止其默认行为
  target.ondragover = function (e) {
    e.preventDefault();
  };
  // 放置元素绑定drop事件

  target.ondrop = function (e) {
    console.log("drop");
    e.preventDefault(); // 阻止默认行为
    e.stopPropagation(); // 阻止事件冒泡
  };
</script>

# 4、DataTransfer 对象

TIP

  • 在事件对象 e 上有一个 dataTransfer 属性,这个属性是一个 DataTransfer 对象
  • e.dataTransfer 对象用于保存拖放(drag and drop)过程中的拖拽数据,可以保存一项或多项数据,这些数据项可以是一种或者多种数据类型。
  • e.dataTransfer 对象提供了相关的属性和方法实现拖放功能

具体如下:

属性 说明
effectAllowed 表示对被拖动元素是否允许 dropEffect 中设置的行为,同时会影响鼠标的样式
dropEffect 可以告诉浏览目标元素允许哪种放置行为, 同时会影响鼠标的样式
方法 说明
setDragImage 可以自定义一处图片元素来设置拖放图片。
setData 方法用来设置拖放操作的drag data (opens new window)到指定的数据和类型
getData 方法接受指定类型的拖放(以DOMString (opens new window)的形式)数据。如果拖放行为没有操作任何数据,会返回一个空字符串
clearData() 删除与给定类型关联的数据,如果类型为空或未指定,则删除与所有类型关联的数据

# 4.1、dropEffect 属性

TIP

  • dropEffect 属性:可以告诉浏览目标元素允许哪种放置行为,比如:复制、移动、导航等,但具体行为的动作还需要开发者自己代码实现。
  • 当拖动元素在目标元素上放置时,只有dropEffect 允许的行为,才会触发 drop 事件
  • dropEffect 属性值,还会会影响拖拽过程中光标的手势,这个手势 可能会预示了将要发生什么样的操作,但仅是视觉上的反馈。

这个属性有以下 4 种可能值

属性值 说明
none 被拖动元素不允许被放置在目标元素,鼠标样式是禁止状态
move 被拖动元素应该移动到放置的目标元素中,但必需满足 dropEffect 的行为是 effectAllowed 允许的行为
copy 被拖动元素应该复制到放置目标元素中,但必需满足 dropEffect 的行为是 effectAllowed 允许的行为
link 放置目标会导航到被拖动元素(仅限它是 URL 情况)

注意事项

  • 对于 dragenterdragover事件,dropEffect 会根据用户的请求的行为进行初始化。具体如何初始化和浏览器平台有关。(即没有设置 dropEffect 属性值时,浏览器会自动为其赋值)
  • 我们期望得到一个指定的行为时而不是用户的请求行为时,可以通过 dragenterdragover 事件处理中修改 dropEffect的值。
<style>
  .drag {
    width: 100px;
    height: 100px;
    background-color: khaki;
  }
  .target {
    width: 200px;
    height: 200px;
    border: 2px dashed #ddd;
    position: absolute;
    top: 10px;
    left: 220px;
  }
</style>

<div class="drag" draggable="true"></div>
<div class="target"></div>

<script>
  var drag = document.querySelector(".drag"); // 被拖动元素
  var target = document.querySelector(".target"); // 目标放置元素

  // 拖动元素进入目标元素那一刻
  target.ondragenter = function (e) {
    // 阻止默认行为,元素才能被放置,才有可能触发drop事件
    e.preventDefault();
    // move 允许被拖动元素移动到目标元素上 , 同时光标也会变化
    e.dataTransfer.dropEffect = "move";
  };

  target.ondragover = function (e) {
    // 阻止默认行为,元素才能被放置,才有可能触发drop事件
    e.preventDefault();
    // move 允许被拖动元素移动到目标元素上 , 同时光标也会变化
    e.dataTransfer.dropEffect = "move";
    // 被动元素不允许放在这里,不会触发drop事件,光标为禁止样式
    // e.dataTransfer.dropEffect = "none";
  };

  target.ondrop = function () {
    console.log("drop");
  };
</script>

# 4.2、effectAllowed 属性

TIP

effectAllowed 属性:表示对被拖动元素是否允许 dropEffect 中设置的行为

属性值 说明
none 不允许 dropEffect 的任何行为
copy 只允许”copy"这种 dropEffect 行为
link 只允许”link"这种 dropEffect 行为
move 只允许”move"这种 dropEffect 行为
copyLink 允许"copy" 和 “link"两种 dropEffect 行为
copyMove 允许"copy" 和 “move"两种 dropEffect 行为
linkMove 允许"link" 和 “move"两种 dropEffect 行为
all 允许所有 dropEffect 行为
uninitialized 效果没有设置时的默认值,则等同于 all

注意事项

  • 应该 在dragstart事件处理函数中设置effectAllowed的属性值,以便为拖动元素设置所需的拖动效果(鼠标样式)
  • 要看到不同属性值对应的鼠标标样式,还需要在document.ondragover事件中,取消默认行为。
  • 因为默认情况下,元素是不允许被放置的,所以在拖动元素时,事件会冒泡到 document 的 ondragover 事件中,鼠标样式为禁用。(整个拖动过程中 document 默认都是被放置元素)
<style>
  .drag {
    width: 100px;
    height: 100px;
    background-color: khaki;
  }
  .target {
    width: 200px;
    height: 200px;
    border: 2px dashed #ddd;
    position: absolute;
    left: 200px;
    top: 10px;
  }
</style>

<div class="drag" draggable="true"></div>
<div class="target"></div>
<script>
  var drag = document.querySelector(".drag");
  var target = document.querySelector(".target");
  drag.ondragstart = function (e) {
    // 设置被拖动元素允许的 dropEffect行为
    e.dataTransfer.effectAllowed = "copy";
  };

  document.ondragover = function (e) {
    // 禁止默认行为,才能看到拖动元素的光标样式与effectAllowed的值对应
    e.preventDefault();
  };
</script>
  • 如果 effectAllowed 的值,不是在 dropEffect 允许的范围,则不会触发 drop 事件
  • 同时拖动元素进入目标元素时,鼠标样式为禁止样式
<script>
  drag.ondragstart = function (e) {
    // 被拖动元素只允许copy这种dropEffect行为
    // 如果dropEffect的值不是copy,则不会触发drop事件,
    // 同时被拖动元素进入有效目标元素时,光禁为禁止
    e.dataTransfer.effectAllowed = "copy";
  };

  // 要看到整个拖动过程中,鼠标光标不为禁止样式
  // 则需要在document.ondragover中禁止默认行为
  document.ondragover = function (e) {
    e.preventDefault();
  };

  //
  target.ondragover = function (e) {
    // 阻止默认行为,target元素才能允许被放置
    e.preventDefault();
    // 但最终哪些dropEffect行为能被放置,会触发drop事件,需要看dropEffect的值。
    // 如果EffectAllowed的值不是在 dropEffect允许内,不会触发drop事件,同时光标为禁止
    // 以下设置 不会触发drop事件,因为effectAllowed=“copy",同是光标为禁止样式
    e.dataTransfer.dropEffect = "move";

    // 以下设置,会触发drop事件,光标为copy样式
    // e.dataTransfer.dropEffect = "copy";
  };

  target.ondrop = function (e) {
    console.log("drop");
  };
</script>

# 4.4、setData 、getData 方法

方法 说明
setData 方法用来设置拖放操作的drag data到指定的数据和类型
getData 方法读取指定类型的拖放(以DOMString的形式)数据。如果拖放行为没有操作任何数据,会返回一个空字符串

语法

e.dataTransfer.setData(format, data); // 保存数据
e.dataTransfer.getData(format); // 取出数据
  • data 表示要存入的数据
  • format 表示存入的数据类型,常见的支持类型如下:

推荐的拖动数据类型参考官网链接地址 (opens new window)

数据类型 说明
text/plain 支持简写:text 文本类型(string)
text/html HTML 文字
text/xml XML 文字
text/uri-list 支持简写: url 链接
application/x-moz-file 文件

注意事项

  • 一般都会在 dragstart 事件中,利用e.dataTransfer.setData() 保存数据,可以存储一项或多项
  • 存储在 dataTransfer 对象中的数据只能在 drop 放置事件中读取。
  • 如果没有在 drop 事件处理函数中取得这些数据,随后 dataTransfer 对象被销毁,数据也就丢失了。
<style>
  .drag {
    width: 100px;
    height: 100px;
    background-color: khaki;
  }
  .target {
    width: 200px;
    height: 200px;
    border: 2px dashed #ddd;
    position: absolute;
    left: 200px;
    top: 10px;
  }
</style>
<div class="drag" draggable="true" id="J_drag"></div>
<div class="target"></div>

<script>
  var drag = document.querySelector(".drag");
  var target = document.querySelector(".target");
  drag.ondragstart = function (e) {
    e.dataTransfer.setData("text", e.target.id);
    // 下面这种情况保存id,在获取时得到的数据为空,因为他会把后面数据当链接处理
    e.dataTransfer.setData("url", e.target.id);
  };

  target.ondragover = function (e) {
    e.preventDefault();
  };

  target.ondrop = function (e) {
    var id = e.dataTransfer.getData("text");
    // var id = e.dataTransfer.getData("url");
    var dragElement = document.getElementById(id);
    target.appendChild(dragElement);
  };
</script>

GIF2022-11-2916-14-25

# 4.5、clearData

TIP

clearData()方法删除给定类型的拖动操作的 drag data,如果给定类型的数据不存在,则此方法不执行任何操作

// 清空对应类型数据  如果不带参数,默认清空所有setData方式添加的数据
e.dataTransfer.clearData([format]);

# 4.6、setDragImage

TIP

setDragImage可以自定义一个 img 类型,用来设置拖放过程中的鼠标下面的图标

setDragImage(element, x, y);
  • element 拖拽时鼠标下面的图片,必需是一个元素节点
  • x 表示:图标距离鼠标指针的 x 轴方向的偏移量
  • y 表示:图标距离鼠标指针 y 轴方向的偏移量 移
<style>
  .drag {
    width: 100px;
    height: 100px;
    background-color: khaki;
  }
  .target {
    width: 200px;
    height: 200px;
    border: 2px dashed #ddd;
    position: absolute;
    left: 200px;
    top: 10px;
  }
</style>

<div class="drag" draggable="true" id="J_drag"></div>
<div class="target"></div>
<script>
  var drag = document.querySelector(".drag");
  var target = document.querySelector(".target");

  drag.ondragstart = function (e) {
    var img = document.createElement("img");
    img.src = "./example.jpg";
    e.dataTransfer.setDragImage(img, 10, 10); // 设置自定义图拖拽图像
  };
</script>

GIF2022-11-2916-30-10

<style>
  .drag {
    width: 100px;
    height: 100px;
    background-color: khaki;
    position: absolute;
    left: 100px;
    top: 100px;
    cursor: move;
  }
</style>
<!-- draggable=true 表示元素可拖拽 -->
<div class="drag" draggable="true"></div>
<script>
  var drag = document.querySelector(".drag");
  var offsetLeft;
  var offsetTop;
  var _clientX;
  var _clientY;

  drag.ondragstart = function (e) {
    offsetLeft = this.offsetLeft;
    offsetTop = this.offsetTop;

    _clientX = e.clientX;
    _clientY = e.clientY;
  };

  drag.ondrag = function (e) {
    var clientX = e.clientX;
    var clientY = e.clientY;

    var left = clientX - _clientX + offsetLeft;
    var top = clientY - _clientY + offsetTop;

    this.style.left = left + "px";
    this.style.top = top + "px";
  };

  document.addEventListener("dragover", function (event) {
    event.preventDefault();
    event.dataTransfer.dropEffect = "move"; // 鼠标样式
  });
</script>

# 四、HTML5 拖拽综合应用案例

TIP

结合本章节所学内容进行综合实践应用。

# 1、双向拖拽添加内容

TIP

涉及知识点

  • 事件委托
  • 拖拽事件(dragstart、dragend、dragover、drop)

GIF2022-11-2917-21-35

<style>
  html,
  body,
  ul,
  li {
    margin: 0;
    padding: 0;
  }
  li {
    list-style: none;
  }
  .container {
    width: 600px;
    margin: 100px auto;
    display: flex;
    justify-content: space-between;
  }
  .select-fruit,
  .like-fruit {
    text-align: center;
    width: 200px;
  }

  .drag-wrap {
    border: 1px solid #ddd;
    padding: 10px;
    height: 280px;
  }
  .drag-wrap li,
  .target-wrap li {
    height: 50px;
    line-height: 50px;
    background-color: #ddd;
    margin: 5px auto;
  }
  .drag-wrap li:hover,
  .target-wrap li:hover {
    background-color: khaki;
  }

  .target-wrap {
    border: 2px dashed #ddd;
    padding: 10px;
    height: 280px;
  }
  .target-wrap li {
    background-color: skyblue;
  }
</style>

<div class="container">
  <!--select-fruit start-->
  <div class="select-fruit">
    <h3>选择你喜欢的水果</h3>
    <ul class="drag-wrap">
      <li draggable="true" class="drag-item">苹果</li>
      <li draggable="true" class="drag-item">香蕉</li>
      <li draggable="true" class="drag-item">梨子</li>
      <li draggable="true" class="drag-item">葡萄</li>
      <li draggable="true" class="drag-item">芒果</li>
    </ul>
  </div>
  <!--select-fruit end-->

  <!-- like-fruit start -->
  <div class="like-fruit">
    <h3>你最喜欢的水果是</h3>
    <ul class="target-wrap"></ul>
  </div>
  <!-- like-fruit end -->
</div>

JS 实现

TIP

  • 在这个案例中,拖动元素的容器本身也是放置目标元素。
  • 放置目标元素中的子元素,本身也是被拖动的元素。
  • 所有拖动元素要处理的事情,全部委托给他到们的父容器来处理。
<script>
  // 拖动元素的容器
  var dragWrap = document.querySelector(".drag-wrap");
  // 目标元素容器
  var targetWrap = document.querySelector(".target-wrap");
  var currentDragEl = null; // 记录当前被拖动的元素

  // 拖动元素  采用事件委托,所有子元素的拖拽行为委托给父元还给来处理
  dragWrap.ondragstart = dragStartHandle;
  dragWrap.ondragend = dragEndHandle;
  dragWrap.ondragover = dragOverHandle;
  dragWrap.ondrop = dragDropHandle;

  // 放置元素
  targetWrap.ondragstart = dragStartHandle;
  targetWrap.ondragend = dragEndHandle;
  targetWrap.ondragover = dragOverHandle;
  targetWrap.ondrop = dragDropHandle;

  // 以下代码,全部采用了事件委托来处理
  function dragStartHandle(e) {
    // 获取被拖动的元素
    var target = e.target;
    // 刚拖动时,元素透明度将低
    target.classList.add("opacity");
    // 记录被拖动的元素
    currentDragEl = target;
  }
  function dragEndHandle(e) {
    var target = e.target;
    // 刚拖动结束,还原透明度
    target.classList.remove("opacity");
  }
  function dragOverHandle(e) {
    // 阻止默认行为,才会触发drop事件
    e.preventDefault();
    // 设置光标样式
    e.dataTransfer.dropEffect = "move";
  }
  function dragDropHandle(e) {
    // 将拖动元素移动到当前目标元素中
    this.appendChild(currentDragEl);
  }
</script>

# 2、拖拽排序

TIP

核心知识

  • 拖拽相关事件
  • 占位思想
  • 找出相邻兄弟中离自己最近的元素
  • insertAdjacentElement 方法
  • prepend 方法

GIF2022-11-300-11-06

# 2.1、CSS 布局代码

<style>
  html,
  body,
  ul,
  li {
    margin: 0;
    padding: 0;
  }
  li {
    list-style: none;
  }
  .drag-wrap {
    width: 200px;
    height: 280px;
    border: 1px solid #ddd;
    padding: 10px;
    margin: 100px;
    position: relative; /* 切记设置为相对定位 */
  }
  .drag-wrap .drag-item {
    height: 50px;
    width: 200px;
    text-align: center;
    line-height: 50px;
    background-color: #ddd;
    margin: 5px 0px;
    cursor: move;
  }
  .drag-wrap .drag-item.dragcss {
    background-color: khaki;
  }
  /* 占位元素样式 */
  .drag-wrap .stance {
    height: 50px;
    margin: 5px 0px;
  }
</style>
<div class="drag-wrap">
  <div class="drag-item" draggable="true">苹果</div>
  <div class="drag-item" draggable="true">香蕉</div>
  <div class="drag-item" draggable="true">梨子</div>
  <div class="drag-item" draggable="true">葡萄</div>
  <div class="drag-item" draggable="true">芒果</div>
</div>

# 2.2、JS 实现原理

TIP

第一步:实现鼠标按下开始拖动时要处理的事情

  • 1、为了后面 drag 事件中能获取到当前拖动的元素,还需要把拖动元素保存到全局变量中
  • 2、把拖动元素的背景色设置为黄色
  • 3、同时把拖动元素设置为绝对定位元素,设置好初始 left 和 top 值,同时把 margin 值为 0
  • 4、创建一个 div 元素,作为占位元素,其宽、高、margin 和被拖动元素一样,然后把创建好的空元素插入当前元素前面。
  • 5、为了后面拖动元素,还需要在此记录鼠标按下时与浏览器左边和上边的距离。

以上拖动元素需要处理的事情,全委托给他们的父元素来处理。

// 拖动的父容器
var dragWrap = document.querySelector(".drag-wrap");
var dragLeft; // 记录被拖动元素的初始left值
var dragTop; // 记录被拖动元素的初始top值
var stanceELement = null; // 用来保存占位元素
var pageX; // 记录拖动元素开始拖动时,鼠标与浏览器的水平距离
var pageY; // 记录拖动元素开始拖动时,鼠标与浏览器的垂直距离
var dragElement = null; // 记录被拖动的元素

// 事件委托  拖动元素需要处理的事情,全很托给他们的父元素来处理
dragWrap.ondragstart = function (e) {
  // 获取被拖动的元素
  var target = e.target;
  // 1、记录被拖动的元素
  dragElement = target;
  // 2、将被拖动的元素背景设置为黄色的
  target.classList.add("dragcss");
  // 3、设置当前被拖动元素为定位元素,相对于他的父元素定位,所以父元素一定要设置相对定位
  // 获取元素相对其定位父元素的left和top值
  dragLeft = target.offsetLeft;
  dragTop = target.offsetTop;
  target.style.position = "absolute";
  target.style.left = dragLeft + "px";
  target.style.top = dragTop + "px";
  target.style.margin = "0px auto";

  // 4、创建占位元素,将其插入当前拖动元素的后面
  stanceELement = document.createElement("div");
  stanceELement.classList.add("stance");
  this.insertBefore(stanceELement, target); // 占位元素插入拖动元素后面

  // 5、记录鼠标按下开始拖动时,鼠标与浏览器左边和顶部的距离
  pageX = e.pageX;
  pageY = e.pageY;
};

第二步:拖动过程中要处理的事情

  • 元素能正常随着鼠标拖动
// 拖动过程中需要处理的事情
dragWrap.ondrag = function (e) {
  // 获取鼠标与浏览器左边和上边距离
  var _pageX = e.pageX;
  var _pageY = e.pageY;

  // 记录鼠标移动的距离
  var moveX = _pageX - pageX;
  var moveY = _pageY - pageY;

  // 动态给拖动元素赋值
  dragElement.style.left = moveX + dragLeft + "px";
  dragElement.style.top = moveY + dragTop + "px";

  // 相关后面drag事件中要处理的代码,从这个位置接上
};

重点提示

整个 drag 操作过程的样式要生效,必需要设置父容器为允许放置行为,即在 dragover 中取消默认行为

dragWrap.ondragover = function (e) {
  e.preventDefault();
};
  • 找到与当前拖动元素同时满足以下条件的兄弟元素(兄弟元素不包含占位元素)

拖动元素底部与浏览器的距离 >= 兄弟元素垂直中心点与浏览器顶部的距离

  • 封装一个函数来实现,如果有对应的兄弟元素就将元素返回,没有就返回 null
/**
 * 寻找具有相同className的兄弟元素中,满足拖元素底部与浏览器距离 > 兄弟元素中心点与浏     览器的距离的元素
 * dragEl 被拖动元素
 * className 通过className来过滤不需要兄弟元素,然后在需要的兄弟元素中找最近的
 */
function findNearSibling(dragEl, className) {
  //  获取拖动元素底部距离浏览器顶部距离
  var rect = dragEl.getBoundingClientRect();
  var dragTop = rect.bottom;

  // 获取拖动元素的所有兄弟元素,包括了自已,还包括了占位符
  // 所以通过className名过滤占位符
  var siblings = dragEl.parentNode.children;
  // 将类数组转成数组
  siblings = Array.prototype.slice.call(siblings, 0);
  // 过滤满足条件的兄弟元素
  siblings = siblings.filter(function (item) {
    return item.classList.contains(className);
  });

  var result = null; // 用来保存找到了满足条件兄弟元素

  // 遍历所有过滤后满足条件的兄弟元素,要排除自身
  for (var i = 0; i < siblings.length; i++) {
    if (siblings[i] === dragEl) continue; // 排除自身
    // 获取每个元素中心与浏览器顶部的距离
    var rect = siblings[i].getBoundingClientRect();
    var center = rect.top + siblings[i].offsetHeight / 2;

    // 找到离自己最近的满足 dragTop>center的兄弟元素
    if (dragTop > center) {
      result = siblings[i];
    } else {
      return result;
    }
  }
  return result;
}
  • 如果有满足条件的兄弟元素,就将兄弟元素的背景色变为红色,同时将占位符插入当前兄弟元素的后面。
  • 如果没有满足条件的兄弟元素,说明当前拖动元素在第一个子元素的前面,此时可以将占位元素插入到父元素的第一个子元素的前面。
// 以下代码写在drag事件中
//  返回符合条件的兄弟元素
var nearSibling = findNearSibling(dragElement, "drag-item");
// 如果不存在满足条件兄弟元素,说明当前拖动元素在第一个子元素的最前面
if (!nearSibling) {
  dragWrap.prepend(stanceELement); // 将占位元素插入到父元素的第一个子元素的前面
}
// 如果存在满足条件的兄弟元素,则需要做以下几件事
if (nearSibling) {
  prevNearSibling && (prevNearSibling.style.backgroundColor = "");
  // 1、将元素背景变为红色
  nearSibling.style.backgroundColor = "red";
  // 3、记录下被(背景变红)的元素为前一个元素
  prevNearSibling = nearSibling;
  // 2、将占位符元素,插入到当前元素的后面
  nearSibling.insertAdjacentElement("afterend", stanceELement);
}

第三步:拖动结束要处理的事情

TIP

  • 拖动元素的背景色还原
  • 拖动元素要取消定位,同时恢复 margin 值
  • 将拖动元素放入占位元素所在位置(前或后)
  • 将占位元素从页面中移除
  • 如果存在前一个背景变红的兄弟元素,将红色背景去掉
// 拖动结束
dragWrap.ondragend = function (e) {
  // 1、拖动元素颜色还原
  dragElement.classList.remove("dragcss");
  // 2、拖动元素取消定位,还原margin值
  dragElement.style.position = "";
  dragElement.style.margin = "5px auto";
  // 3、插入占位元素的后面
  dragWrap.insertBefore(dragElement, stanceELement);
  // 4、移除占位元素
  dragWrap.removeChild(stanceELement);
  // 5、前一个元素存在,则移出样式
  prevNearSibling && (prevNearSibling.style.backgroundColor = "");
};

# 2.3、JS 完整源码

<script>
  // 拖动的父容器
  var dragWrap = document.querySelector(".drag-wrap");
  var dragLeft; // 记录被拖动元素的初始left值
  var dragTop; // 记录被拖动元素的初始top值
  var stanceELement = null; // 用来保存占位元素
  var pageX; // 记录拖动元素开始拖动时,鼠标与浏览器的水平距离
  var pageY; // 记录拖动元素开始拖动时,鼠标与浏览器的垂直距离
  var dragElement = null; // 记录被拖动的元素
  var prevNearSibling = null; // 前一个背景变红的元素

  // 事件委托  拖动元素需要处理的事情,全很托给他们的父元素来处理
  dragWrap.ondragstart = function (e) {
    // 获取被拖动的元素
    var target = e.target;
    // 1、记录被拖动的元素
    dragElement = target;
    // 2、将被拖动的元素背景设置为黄色的
    target.classList.add("dragcss");
    // 3、设置当前被拖动元素为定位元素,相对于他的父元素定位,所以父元素一定要设置相对定位
    // 获取元素相对其定位父元素的left和top值
    dragLeft = target.offsetLeft;
    dragTop = target.offsetTop;
    target.style.position = "absolute";
    target.style.left = dragLeft + "px";
    target.style.top = dragTop + "px";
    target.style.margin = "0px auto";

    // 4、创建占位元素,将其插入当前拖动元素的后面
    stanceELement = document.createElement("div");
    stanceELement.classList.add("stance");
    this.insertBefore(stanceELement, target); // 占位元素插入拖动元素后面

    // 5、记录鼠标按下开始拖动时,鼠标与浏览器左边和顶部的距离
    pageX = e.pageX;
    pageY = e.pageY;
  };

  // 拖动过程中需要处理的事情
  dragWrap.ondrag = function (e) {
    // 获取鼠标与浏览器左边和上边距离
    var _pageX = e.pageX;
    var _pageY = e.pageY;

    // 记录鼠标移动的距离
    var moveX = _pageX - pageX;
    var moveY = _pageY - pageY;

    // 动态给拖动元素赋值
    dragElement.style.left = moveX + dragLeft + "px";
    dragElement.style.top = moveY + dragTop + "px";

    //  返回符合条件的兄弟元素
    var nearSibling = findNearSibling(dragElement, "drag-item");
    // 如果不存在满足条件兄弟元素,说明当前拖动元素在第一个子元素的最前面
    if (!nearSibling) {
      dragWrap.prepend(stanceELement); // 将占位元素插入到父元素的第一个子元素的前面
    }
    // 如果存在满足条件的兄弟元素,则需要做以下几件事
    if (nearSibling) {
      prevNearSibling && (prevNearSibling.style.backgroundColor = "");
      // 1、将元素背景变为红色
      nearSibling.style.backgroundColor = "red";
      // 3、记录下被(背景变红)的元素为前一个元素
      prevNearSibling = nearSibling;
      // 2、将占位符元素,插入到当前元素的后面
      nearSibling.insertAdjacentElement("afterend", stanceELement);
    }
  };

  dragWrap.ondragover = function (e) {
    e.preventDefault();
  };

  // 拖动结束
  dragWrap.ondragend = function (e) {
    // 1、拖动元素颜色还原
    dragElement.classList.remove("dragcss");
    // 2、拖动元素取消定位,还原margin值
    dragElement.style.position = "";
    dragElement.style.margin = "5px auto";
    // 3、插入占位元素的后面
    dragWrap.insertBefore(dragElement, stanceELement);
    // 4、移除占位元素
    dragWrap.removeChild(stanceELement);
    // 5、前一个元素存在,则移出样式
    prevNearSibling && (prevNearSibling.style.backgroundColor = "");
  };

  /**
   * 寻找具有相同className的兄弟元素中,满足拖元素底部与浏览器距离 > 兄弟元素中心点与浏     览器的距离的元素
   * dragEl 被拖动元素
   * className 通过className来过滤不需要兄弟元素,然后在需要的兄弟元素中找最近的
   */
  function findNearSibling(dragEl, className) {
    //  获取拖动元素底部距离浏览器顶部距离
    var rect = dragEl.getBoundingClientRect();
    var dragTop = rect.bottom;

    // 获取拖动元素的所有兄弟元素,包括了自已,还包括了占位符
    // 所以通过className名过滤占位符
    var siblings = dragEl.parentNode.children;
    // 将类数组转成数组
    siblings = Array.prototype.slice.call(siblings, 0);
    // 过滤满足条件的兄弟元素
    siblings = siblings.filter(function (item) {
      return item.classList.contains(className);
    });

    var result = null; // 用来保存找到了满足条件兄弟元素

    // 遍历所有过滤后满足条件的兄弟元素,要排除自身
    for (var i = 0; i < siblings.length; i++) {
      if (siblings[i] === dragEl) continue; // 排除自身
      // 获取每个元素中心与浏览器顶部的距离
      var rect = siblings[i].getBoundingClientRect();
      var center = rect.top + siblings[i].offsetHeight / 2;

      // 找到离自己最近的满足 dragTop>center的兄弟元素
      if (dragTop > center) {
        result = siblings[i];
      } else {
        return result;
      }
    }
    return result;
  }
</script>

# 五、重难点总结

TIP

总结本章重难点知识,理清思路,把握重难点。并能轻松回答以下问题,说明自己就真正的掌握了。

用于故而知新,快速复习。

# 1、重点

TIP

  • 封装处理鼠标滚轮事件的兼容性函数
  • 掌握禁止右键菜和禁止选中元素
  • 如何识别用户按下的是那个鼠标键
  • 掌握 drag 拽拖事件(drag、dragstart、dragend、dragenter、dragover、dragleave、drpp)
  • 理解 dropEffect 和 effectAllowed 属性和 setData、getData、clearData 方法

# 2、难点

TIP

手写以下三个案例

  • 全屏垂直滚动轮播
  • 双向拖拽添加内容
  • 拖拽排序
上次更新时间: 6/8/2023, 9:23:17 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X