# 拖拽

利用drag和drop等api我们可以实现DOM元素在页面上的拖拽行为,利用这些功能我们可以用于自定义布局块,文件拖拽导入上传,列表顺序调整等功能。

# 1、draggable

当我们给一个元素设置 draggable属性为true,表明这个元素是可拖拽的, 浏览器会默认给一个可拖拽的效果

<div class="draggable-box" draggable="true">I am draggable</div>

# 2、作用于可拖拽元素的事件

有一些事件是可以用于可拖拽元素的, 分别是 dragstart, drag, dragend, dragover

  1. dragstart 当用户开始拖动一个元素或者一个选择文本的时候 dragstart 事件就会触发。
  2. drag当元素或者选择的文本被拖动时触发 drag 事件 (每几百毫秒). 只要一直拖着不放就会一直触发
  3. dragend拖放事件在拖放操作结束时触发 (通过释放鼠标按钮或单击 escape 键)。

# 3、作用于放置区域的事件

  1. dragenter当拖动的元素或被选择的文本进入有效的放置目标时, dragenter 事件被触发。
  2. dragleave当一个被拖动的元素或者被选择的文本离开一个有效的拖放目标时,将会触发dragleave 事件。
  3. dragover当元素或者选择的文本被拖拽到一个有效的放置目标上时,触发 dragover 事件(每几百毫秒触发一次)。这个有些疑问 有效的放置目标 不知道是什么意思, 观察到的是在可拖拽元素自身上时会一直触发。注册这些事件的元素也会被触发, 可以查看demo
  4. drop当一个元素或是选中的文字被拖拽释放到一个有效的释放目标位置时,drop 事件被抛出。

WARNING

从实际操作来看, 要想drop生效,dragover事件也需同时绑定,同时可以设置阻止冒泡和阻止默认行为,否则可能不生效。

<!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>Document</title>
    <style>
      .draggable-box {
        padding: 8px 12px;
        background-color: #bbb;
        width: 200px;
        margin-bottom: 12px;
      }
      .dropzone {
        width: 400px;
        height: 400px;
        margin: 20px;
        border: 2px solid #ccc;
        transition: all 0.25s ease;
      }
    </style>
  </head>
  <body>
    <div id="dragBox" class="draggable-box" draggable="true">I am draggable</div>
    <div class="draggable-box" draggable="false">I am not draggable</div>
    <div style="display: flex">
      <div class="dropzone">
        <span>drop here</span>
      </div>
      <div class="dropzone">
        <span>drop here</span>
      </div>
    </div>
    <script>
      const dragBox = document.querySelector('#dragBox');
      const dropzones = document.querySelectorAll('.dropzone');
      dragBox.addEventListener('dragstart', e => {
        console.log(e);
        e.target.style.opacity = '0.5';
      });
      dragBox.addEventListener('drag', e => {
        e.preventDefault();
      });
      /* dragBox.addEventListener('dragover', e => {
        e.preventDefault();
      }); */
      dragBox.addEventListener('dragend', e => {
        e.preventDefault();
        console.log('dragend', e);
        e.target.style.opacity = '';
      });
      /* 采用代理是更合理的方法 */
      for (let i = 0, len = dropzones.length; i < len; i++) {
        const dropzone = dropzones[i];
        dropzone.addEventListener('dragenter', e => {
          e.preventDefault();
          e.stopPropagation();
          // 可以做一些高亮的操作
          e.target.style.borderColor = 'blue';
        });
        dropzone.addEventListener('dragover', function (e) {
          e.preventDefault();
          e.stopPropagation();
        });
        dropzone.addEventListener('dragleave', e => {
          e.preventDefault();
          e.stopPropagation();
          // 可以做一些样式重置的操作
          e.target.style.borderColor = '';
        });
        dropzone.addEventListener('drop', e => {
          console.log('drop');
          // 阻止一些默认事件, 比如图片预览,打开链接等功能
          e.preventDefault();
          e.target.appendChild(dragBox);
        });
      }
    </script>
  </body>
</html>

# 4、数据传递

有时候把一个DOM从一个地方拖拽放置到另外一个地方时,除了一些DOM操作之外, 我们可能还想要传递一些额外的数据。

这个时候可以利用 event.dataTransfer.setData 设置一些数据传递, 经测试可以set多个数据。DataTransfer.setData() 方法用来设置拖放操作的drag、drop到指定的数据和类型。

获取数据可以使用 DataTransfer.setData()

dragBox.addEventListener('dragstart', e => {
  console.log(e);
  event.dataTransfer.setData('objData', JSON.stringify({ x: 1 }));
  event.dataTransfer.setData('strData', 'test');
  e.target.style.opacity = '0.5';
});

dropzone.addEventListener('drop', e => {
  console.log('drop');
  // 阻止一些默认事件, 比如图片预览,打开链接等功能
  e.preventDefault();
  const objData = event.dataTransfer.getData('objData');
  const strData = event.dataTransfer.getData('strData');
  console.log(objData, strData);
  e.target.appendChild(dragBox);
});

TIP

setData的MIME数据类型只能是"text/plain" 和 "text/uri-list"。 也就意味着无法传递引用对象等类型

来一个例子 (opens new window)

# 5、DataTransfer.dropEffect

DataTransfer.dropEffect属性用来设置放下(drop)被拖拉节点时的效果,会影响到拖拉经过相关区域时鼠标的形状。它可能取下面的值。

  • copy:复制被拖拉的节点
  • move:移动被拖拉的节点
  • link:创建指向被拖拉的节点的链接
  • none:无法放下被拖拉的节点
target.addEventListener('dragover', e => {
  e.preventDefault();
  e.stopPropagation();
  e.dataTransfer.dropEffect = 'copy';
});

# 6、文件上传

还是采用drop 这个事件,要点就是知道可以从 event.dataTransfer.files 这个属性获取到FileList数据。

可以直接看下下面的demo (opens new window)

<!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>
    <style>
      .wrapper {
        width: 200px;
        height: 200px;
        border: 1px dashed #ccc;
        position: relative;
      }
      .wrapper::after {
        content: '+';
        position: absolute;
        font-size: 30px;
        color: #ccc;
        width: 30px;
        height: 30px;
        line-height: 30px;
        text-align: center;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
      }
    </style>
  </head>
  <body>
    <div class="wrapper"></div>
    <script>
      const wrapper = document.querySelector('.wrapper');
      wrapper.addEventListener(
        'dragenter',
        function (e) {
          e.preventDefault();
          e.stopPropagation();
          e.target.style.borderColor = 'red';
        },
        false
      );

      wrapper.addEventListener(
        'dragover',
        function (e) {
          e.preventDefault();
          e.stopPropagation();
        },
        false
      );

      wrapper.addEventListener(
        'dragleave',
        function (e) {
          e.preventDefault();
          e.stopPropagation();
          e.target.style.borderColor = '';
        },
        false
      );

      wrapper.addEventListener('drop', e => {
        e.preventDefault();
        e.stopPropagation();
        e.target.style.borderColor = '';
        const files = e.dataTransfer.files;
        console.log(files);
        for (let i = 0, len = files.length; i < len; i++) {
          const p = document.createElement('p');
          p.textContent = files[i].name;
          document.body.appendChild(p);
        }
      });
    </script>
  </body>
</html>

说一句经验中的题外话,如果是多选上传, 有些后端写的接口不支持并发上传多个,可以采用串行的办法. 就是上传完其中一个文件再上传下一个(串行上传)

function uploadFile(file) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(file.name || file);
    }, 1000);
  });
}

function multipleUpload(fileList, cb) {
  return new Promise((resolve, reject) => {
    const result = [];
    const files = Array.prototype.slice.call(fileList);
    upload();
    async function upload() {
      const file = files.shift();
      try {
        const res = await uploadFile(file);
        result.push(res);
        if (typeof cb === 'function') {
          cb(res);
        }
        if (result.length === fileList.length) {
          resolve(result);
          return;
        }
        if (files.length > 0) {
          upload();
        }
      } catch (error) {
        reject(error);
      }
    }
  });
}

# 7、元素拖移

可能也有类似一个弹窗, 需要移来移去的需求,这个其实更多的借助mousemove 实现可以查看下面的Demo。 如果是移动端, 则可以采用touch事件来实现

<!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>move</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      .box {
        width: 200px;
        height: 100px;
        background-color: #ccc;
        position: fixed;
        left: 50%;
        top: 50%;
        /* transform: translate(-50%, -50%); */
        /* cursor: move;    */
      }
    </style>
  </head>
  <body>
    <button onclick="handleMakeMoveable()">make Moveable</button>
    <button onclick="makeUnmovable()">make Unmovable</button>
    <div class="box"></div>
    <script>
      const box = document.querySelector('.box');

      function makeMoveable(el) {
        if (!el) return () => {};
        el.onmousedown = e => {
          el.style.cursor = 'move';
          const disX = e.clientX - el.offsetLeft;
          const disY = e.clientY - el.offsetTop;

          document.onmousemove = moveEvent => {
            console.log(moveEvent.clientX, moveEvent.clientY);
            let left = moveEvent.clientX - disX;
            if (left < 0) {
              left = 0;
            }
            const rightBoundary = window.innerWidth - el.offsetWidth;
            if (left > rightBoundary) {
              left = rightBoundary;
            }
            let top = moveEvent.clientY - disY;
            if (top < 0) {
              top = 0;
            }
            const bottomBoundary = window.innerHeight - el.offsetHeight;
            if (top > bottomBoundary) {
              top = bottomBoundary;
            }
            el.style.left = left + 'px';
            el.style.top = top + 'px';

            if (
              moveEvent.clientX <= 0 ||
              moveEvent.clientY <= 0 ||
              moveEvent.clientX >= window.innerWidth ||
              moveEvent.clientY >= window.innerHeight
            ) {
              document.onmousemove = null;
            }
          };
          document.onmouseup = upEvent => {
            el.style.cursor = '';
            document.onmousemove = null;
            el.onmouseup = null;
          };
        };

        return () => {
          el.onmousedown = null;
          document.onmousemove = null;
          document.onmouseup = null;
        };
      }
      let cancel = makeMoveable(box);

      function handleMakeMoveable() {
        cancel = makeMoveable(box);
      }
      function makeUnmovable() {
        cancel();
      }
    </script>
  </body>
</html>

上次更新: 1/22/2025, 9:39:13 AM