拖放事件

关于拖放事件有些是在被拖动元素上触发的,而有些则是在放置目标上触发的

当我们拖动某个元素时,会依次触发:

  • ondragstart
  • ondrag
  • ondragend

这三个事件都是在被拖动元素上触发的。当拖动开始时会先触发dragstart事件,然后在拖动的过程中会持续触发drag事件,当拖动停止时(无论被拖动元素是否放到了有效的放置目标)都会触发dragend事件,这三个事件类似鼠标的移动事件mousestart,mousemove,mouseend

当某个元素被拖动到放置目标上,会依次触发:

  • dragenter
  • dragover
  • dragleave 或 drop

这三个事件都是在放置目标上触发的。当元素进入放置目标时会触发dragenter事件,当元素在放置目标上移动时会持续触发dragover事件,当元素移出放置目标时会触发dragleave事件,当元素被放到了放置目标中会触发drop事件而不是dragleave事件,这几个事件(除drop)也类似鼠标的移动事件mouseenter,mouseover,mouseleave

虽然所有的元素都支持drop事件,但是这些元素默认是不允许放置的,这个时候当我们在放置目标上松开鼠标是不会触发drop事件的,我们可以通过event.preventDefault()来阻止默认的行为,如下:

droptarget.ondragenter = event => {
    event.preventDefault()
}
droptarget.ondragover = event => {
    event.preventDefault()
}

另外在一些浏览器中,当我们移动图片到放置目标上,松开的时候会打开这张图片,如果移动的是超链接,则会打开这个页面。我们有时候需要阻止这种默认的行为,可以这样做

droptarget.ondrop = event => {
    event.preventDefault()
}

dataTransfer对象

dataTransfer对象用来在拖动的过程中从被拖动元素向放置目标传递数据,这个对象有两个方法setDatagetData

setData有两个参数,第一个是MIME类型,第二个则是我们要保存的值

event.dataTransfer.setData('text/plain', 'msg')
event.dataTransfer.setData('text/uri-list', 'http://baidu.com')

getData只有一个参数,就是setData中我们传的第一个参数

event.dataTransfer.getData('text/plain')
event.dataTransfer.setData('text/uri-list')

setData我们一般在dragstart中去使用,而getData只能在drop事件中去使用,这个务必记住

你拖动一个图片到目标区域,那目标区域怎么获取这个图片的信息呢?就靠它!它是事件对象的一个属性,用于从被拖动元素向放置目标传递字符串格式的数据。
image.png

复制粘贴事件

  • oncopy 事件在用户拷贝元素上的内容时触发。
  • onbeforecut 事件在用户剪切文本,且文本还未删除时触发触发。
  • oncut 事件在用户剪切元素的内容时触发。
  • onbeforepaste 事件在用户向元素中粘贴文本之前触发。
  • onpaste 事件在用户向元素中粘贴文本时触发。

clipboardData对象

粘贴事件提供了一个clipboardData的属性,它实际上是一个DataTransfer类型的对象,DataTransfer是拖动产生的一个对象,但实际上粘贴事件也是它。
image.png

onPaste绑定的元素必须聚焦后才能触发粘贴方法

本文是将onpaste绑定在div元素上,发现复制一张图片到div元素区域内粘贴时无法触发onpaste事件,必须要点击下该div后粘贴才能触发。

通常是将onpaste绑定在input元素上,聚焦后才能粘贴文本。上面点击div其实也就是聚焦行为。但是div这种非表单元素是不能直接通过focus()来使其聚焦的,需要在div元素上增加contentEditable属性先使其变成可编辑元素(该类元素可以使用focus来聚焦),然后给div绑定onmouseenteronmouseleave方法,鼠标移入时使其聚焦,移出时失焦,这样就能在鼠标移入div区域时粘贴触发onpaste事件。

react项目中控制台可能会报"A component is contentEditable and contains children managed by React"错误,在元素上加上suppressContentEditableWarning 属性就可以了。

聚焦后div元素会出现光标

div上加上如下样式即可:

{
    outline: none;    
    color:transparent; 
    text-shadow:0 0 0 #000;
}

SparkDM5校验图片重复性

使用图片文件对象申城hash来判断图片是否一样,避免重复提交相同的图片到服务器中,SparkMD5可以帮助生成文件hash

image.png

示例

第一步:先写一个简单页面

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="./index.css">
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/layer/3.1.1/layer.min.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.0/spark-md5.js"></script>
</head>
<body>
  <div id="drag-show">
    <span class="title">图片展示区:</span>
    <span class="show-area"></span>
  </div>
  <!-- 图片拖拽区 -->
  <div id="drag-area" contentEditable="true" ondragenter="drag(this, event);" ondragover="drag(this, event);" ondrop="dragUpload(this, event)" onpaste="pasteUpload(this, event);" onmouseenter="handleMouseEnter(this)" onmouseleave="handleMouseLeave(this)">
    <span class="text">将图片拖拽至此处上传</span>
    <div class="upload-box">
      <input type="file" name="file" multiple="multiple" id="file" style="display: none;" onchange="fileChange(this, event);"/>
      <span id="upload" onclick="uploadFile()"></span>
    </div>
  </div>
  <!-- 脚本 -->
  <script src="./index.js"></script>
</body>
</html>

实现脚本

实现拖拽功能

图片拖拽区元素监听ondragenter、ondragover、ondrop事件,监听ondragenter、ondragover事件的目的是防止拖拽图片时执行浏览器默认行为,通过浏览器打开本地图片。ondrop事件可以通过dataTransfer拿到图片文件对象。

/** 防止拖拽图片时打开图片 */
function drag(that, event) {
  var e = event || window.event;
  e.preventDefault()
  e.stopPropagation()
}

/** 拖拽上传 */
function dragUpload(that, event) {
  var e = event || window.event;
  e.preventDefault()
  e.stopPropagation()
  const files = e.dataTransfer.files;
  upload(files);
}
实现复制粘贴

图片拖拽区元素监听onpaste事件,通过clipboardData拿到图片文件对象

function pasteUpload(that, event) {
  var e = event || window.event;
  e.preventDefault()
  e.stopPropagation()
  var files = e.clipboardData.files;
  upload(files);
}
封装文件上传方法,校验图片

拿到图片文件对象后,需要校验图片大小、数量、重复性以及是否压缩等等,这些操作我们都封装到upload中。

在SparkDM5中有appendBinary方法,该方法接受一个字符串,生成一个与该字符串的映射hash值。而图片都可以转化成Data URL,不同的图片Data URL不同,图片越大,Data URL也就越长,但是唯一的。

拓展Data URL知识:
Data URL是一项特殊的技术,可以将资料(例如图片)内嵌在网页之中,不用放到外部文件。使用Data URL的好处是,您不需要额外再发出一个HTTP 请求到服务器端取得额外的资料;而缺点便是,网页的大小可能会变大。它适合应用在内嵌小图片,不建议将大图像文件编码成Data URL来使用。您的图像文件不能够超过浏览器限定的大小,否则无法读取图像文件。

FileReader对象的readAsDataURL方法可以将读取到的文件编码成Data URL需要注意的是读取是异步的

/** 图片存放数组 */
const img_arr = [];
/** 图片上传 */
function upload(files) {
  const MAX_COUNT = 3;
  const IMG_COUNT = MAX_COUNT - img_arr.length;
  const additional_arr = [];
  const IMG_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
  if (Array.prototype.some.call(files, function(file) {return !IMG_TYPES.includes(file.type)})) {
    layer.msg("图片格式不正确!!!")
    $("#file").val('');
    return;
  }

  if (files.length > IMG_COUNT) {
    layer.msg(`最多只能上传${MAX_COUNT}张图片!`)
    $("#file").val('');
    return;
  }

  Array.prototype.forEach.call(files, function(file, i, arr) {
    const fileReader = new FileReader();
    const sparkMD5 = new SparkMD5();
    fileReader.readAsDataURL(file);
    fileReader.onload = (event) => {
      const imgDataUrl =  event.target.result; // 得到data url
      // 生成图片hash
      const hash = sparkMD5.appendBinary(imgDataUrl).end();
      // 判断是否存在相同的图片
      if (!isExists(hash)) {
        additional_arr.push({
          hash,
          src: binary,
          file
        });
      } else {
        // 中断文件流
        fileReader.abort();
        layer.msg("请不要上传相同的图片!!!");
      }
    };
    fileReader.onloadend = (event) => {
      // 读取完成
      if (additional_arr.length === arr.length) {
        img_arr.push(...additional_arr);
        showImg(additional_arr); // 展示在展览区
        $("#file").val('');
      }
    }
  })
} 

// 判断是否有相同的图片
function isExists(hash) {
  return img_arr.some(function(item) {
    return item.hash === hash;
  })
}

附上全部脚本代码和样式

/** 图片存放数组 */
const img_arr = [];
/** 图片上传 */
function upload(files) {
  const MAX_COUNT = 3;
  const IMG_COUNT = MAX_COUNT - img_arr.length;
  const additional_arr = [];
  const IMG_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
  if (Array.prototype.some.call(files, function(file) {return !IMG_TYPES.includes(file.type)})) {
    layer.msg("图片格式不正确!!!")
    $("#file").val('');
    return;
  }

  if (files.length > IMG_COUNT) {
    layer.msg(`最多只能上传${MAX_COUNT}张图片!`)
    $("#file").val('');
    return;
  }

  Array.prototype.forEach.call(files, function(file, i, arr) {
    const fileReader = new FileReader();
    const sparkMD5 = new SparkMD5();
    fileReader.readAsDataURL(file);
    fileReader.onload = (event) => {
      const binary =  event.target.result; // 得到data url
      // 生成图片hash
      const hash = sparkMD5.appendBinary(binary).end();
      // 判断是否存在相同的图片
      if (!isExists(hash)) {
        additional_arr.push({
          hash,
          src: binary,
          file
        });
      } else {
        // 中断文件流
        fileReader.abort();
        layer.msg("请不要上传相同的图片!!!");
      }
    };
    fileReader.onloadend = (event) => {
      // 读取完成
      if (additional_arr.length === arr.length) {
        img_arr.push(...additional_arr);
        showImg(additional_arr); // 展示在展览区
        $("#file").val('');
      }
    }
  })
} 

// 判断是否有相同的图片
function isExists(hash) {
  return img_arr.some(function(item) {
    return item.hash === hash;
  })
}

/** 控制删除icon */
function imgMouseEnter(that) {
  $(that).find("span.del-icon").css('display', 'block');
};

function imgMouseLeave(that) {
  $(that).find("span.del-icon").css('display', 'none');
}

/** 展示图片 */
function showImg(additional_arr) {
  if (!additional_arr.length) {
    return;
  }

  const img_html = additional_arr.reduce(function(init, item) {
    init += `<span class="img-box" onmouseenter="imgMouseEnter(this);" onmouseleave="imgMouseLeave(this);">
      <span class="img-span"><img src="${item.src}" mode="widthFix" class="img-item" ondrag="imgDragOver(this, event);" ondragend="imgDragEnd(this, event);"/></span>
      <span class="del-icon" hash="${item.hash}" onclick="deleteImg(this)" style="display: none;"></span>
    </span>`
    return init;
  }, '');

  $(".show-area").append(img_html);
}

// 删除
function deleteImg(that) {
  const hash = $(that).attr("hash");
  const $imgBox = $(that).parents(".img-box");
  $imgBox.remove();
  img_arr.splice(img_arr.findIndex(function(item) {
    return item.hash === hash;
  }), 1);
}

// 获取元素位置
function getElementLeft(element){
 var actualLeft = element.offsetLeft;
 var current = element.offsetParent;

 while (current !== null){
   actualLeft += current.offsetLeft;
   current = current.offsetParent;
 }
 return actualLeft;
}

function getElementTop(element){
 var actualTop = element.offsetTop;
 var current = element.offsetParent;

 while (current !== null){
  actualTop += current.offsetTop;
  current = current.offsetParent;
 }
 return actualTop; 
}


// 拖拽删除
function imgDragOver(that, event) {
  console.log('start', event)
  const $showArea = $(that).parents(".show-area");
  $showArea.css("border", "1px solid #0065ff")
}

function imgDragEnd(that, event) {
  const hash = $(that).attr("hash");
  const $showArea = $(".show-area");
  const $imgBox = $(that).parents(".img-box");
  const width = $showArea[0].clientWidth;
  const height = $showArea[0].clientHeight;
  const origin = {
    x: getElementLeft($showArea[0]),
    y: getElementTop($showArea[0])
  };

  const target = {
    x: event.clientX,
    y: event.clientY
  }

  if (target.x - origin.x > width || target.x < origin.x || target.y - origin.y > height || target.y < origin.y) {
    $imgBox.remove();
    img_arr.splice(img_arr.findIndex(function(item) {
      return item.hash === hash;
    }), 1);
  }
  $showArea.css("border", "1px dashed #c3c3c3")
  console.log(width, height, origin, target)
}

/** 手动上传 */
function uploadFile() {
  $("#file").click();
}

function fileChange(that, event) {
  const files = event.target.files;
  upload(files);
}

/** 防止拖拽图片时打开图片 */
function drag(that, event) {
  var e = event || window.event;
  e.preventDefault()
  e.stopPropagation()
}

/** 拖拽上传 */
function dragUpload(that, event) {
  var e = event || window.event;
  e.preventDefault()
  e.stopPropagation()
  const files = e.dataTransfer.files;
  upload(files);
}

/** 赋值粘贴上传 */
function handleMouseEnter(that) {
  $(that).focus();
}

function handleMouseLeave(that) {
  $(that).blur();
}

function pasteUpload(that, event) {
  var e = event || window.event;
  e.preventDefault()
  e.stopPropagation()
  var files = e.clipboardData.files;
  upload(files);
}

css

html, body {
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

#drag-show {
  padding-top: 40px;
  line-height: 60px;
  text-align: center;
}

.show-area {
  display: inline-block;
  height: 60px;
  line-height: 60px;
  width: 180px;
  border: 1px dashed #c3c3c3;
  border-radius: 5px;
  vertical-align: middle;
}

.title {
  display: inline-block;
  line-height: 60px;
  color: #555;
  font-size: 20px;
  vertical-align: middle;
}

.img-box {
  position: relative;
  display: inline-block;
  width: 50px;
  height: 50px;
  line-height: 50px;
  margin-right: 8px;
  vertical-align: middle;
  cursor: pointer;
}

.img-span {
  display: inline-block;
  width: 100%;
  height: 100%;
  vertical-align: middle;
  overflow: hidden;
}

.img-item {
  display: inline-block;
  width: 100%;
  vertical-align: middle;
}

.del-icon {
  position: absolute;
  top: -7px;
  right: -7px;
  width: 14px;
  height: 14px;
  background-image: url('./del.png');
  background-size: 100% 100%;
  cursor: pointer;
}

#drag-area {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  left: 50%;
  width: 300px;
  height: 200px;  
  margin-left: -150px;
  margin-top: 20px;
  border-radius: 12px;
  border: 1px dashed #c3c3c3;
  outline: none;    
  color: transparent; 
  text-shadow: 0 0 0 #000;
}

.text {
  color: #555;
  font-size: 20px;
}

.upload-box {
  display: inline-block;
  width: auto;
  height: auto;
}


#upload {
  display: inline-block;
  width: 50px;
  height: 50px;
  padding: 8px;
  margin-top: 10px;
  box-sizing: border-box;
  border-radius: 5px;
  border: 1px dashed #c3c3c3;
  background-size: 100% 100%;
  background-image: url('./plus.png');
  cursor: pointer;
}

#upload:hover {
  background-color: #fafafa;
}

效果:
upload2.gif

paste.gif

参考:
https://blog.csdn.net/deng145...
https://blog.csdn.net/HPFBoy/...


浪遏飞舟
1.9k 声望4.5k 粉丝