前言

说到元素拖拽,一般都会先想到用 HTML5 的拖拽放置 (Drag 和 Drop) 来完成,它提供了一套完好的事情机制,看起来似乎是首选的解决方案,但实践却不是那么美好,首要是它的样式过分粗陋,无法完成更高档的用户体会:

原生拖拽太拉跨了,纯JS自己手写一个拖拽作用,纵享丝滑

这是浏览器默认的拖拽作用,点住拖拽恣意图片或文字都会产生。

笔者因为之前有个小项目需求常常参阅稿定规划,一向有留心其元素拖拽的作用(如下图),所以接下来我将以这种作用为蓝本,运用原生 JS 完成一个赋有动感的 自定义拖拽 作用,话不多说直接开摸。

原生拖拽太拉跨了,纯JS自己手写一个拖拽作用,纵享丝滑

完成原理

首先说下思路,咱们需求知道鼠标的三个事情,分别是 mousedownmousemovemouseup ,当点击按下的时分,克隆一个肯定定位的元素,并标识下”拖拽中”的状况,接着在 mousemove 中就能够判别应该履行的具体办法,从而让元素跟着鼠标移动起来。

在监听事情的 event 方针中,有几个参数是比较重要的:clientXclientY 标识的鼠标当时横坐标和纵坐标,offsetXoffsetY 表明相对偏移量,能够在 mousedown 鼠标按下时记载初始坐标,在 mouseup 鼠标抬起时判别是否在方针区域中,如果是则用鼠标获取到的当时的偏移量 – 初始坐标得到元素实践在方针区域中的方位。

为便利阅读,以下所有代码均有部分省掉,且演示 GIF 会掉帧,非常主张检查码上阅读完好源码合作本文食用,代码量并不多。

根底界面

先简单完成一个两栏布局界面,并应用上一些 CSS 作用:

<div id="app">
  <div class="slide">
    <div id="list">
      <img class="item" src="......." />
      <img  .........
    </div>
  </div>
  <div class="content"></div>
</div>
#app {
  width: 100vw;
  height: 100vh;
  display: flex;
}
.active {
  cursor: grabbing;
}
.slide {
  width: 260px;
  height: 100%;
  overflow: scroll;
  border-right: 1px solid rgba(0,0,0,.15);
  #list {
    user-select: none;
    .item {
      background: rgba(0,0,0,.15);
      width: 120px;
      display: inline-block;
      break-inside: avoid;  
      margin-bottom: 4px;
    }
    .item:hover {
      cursor: grab;
      filter: brightness(90%);
    }
    .item:active {
      cursor: grabbing;
    }
  }
  .grid {
      column-count: 2;
      column-gap: 0px;
  }
}
.slide::-webkit-scrollbar {
  display: none; /* Chrome Safari */
}
#content {
  position: relative;
  flex: 1;
  height: 100%;
  margin-left: 45px;
  background: rgba(0,0,0,.07);
  .item {
    position: absolute;
    transform-origin: top left;
  }
}

运用滤镜 filter: brightness(90%); 调节明亮度能够快速完成一个鼠标掩盖的动态作用,无需额外制造遮罩:

原生拖拽太拉跨了,纯JS自己手写一个拖拽作用,纵享丝滑

运用伪类激活 cursorgrabgrabbing 能够设置抓取动作的图标:

原生拖拽太拉跨了,纯JS自己手写一个拖拽作用,纵享丝滑

完成元素抓取

运用事情托付机制为选择列表添加 mousedown 事情监听,完成抓取的原理是在鼠标按下时克隆按下的元素,并把克隆出来的元素设置成肯定定位,让它”浮”起来:

let dragging = false
let cloneEl = null // 克隆元素
let initial = {} // 初始化数据记载
......
// 选中了元素
cloneEl = e.target.cloneNode(true) // 克隆元素
cloneEl.classList.add('flutter') // 使其起浮
e.target.parentElement.appendChild(cloneEl) // 加入到列表中
dragging = true // 符号拖动开端
// TODO: 初始化克隆元素的定位并记载,便利后面移动时核算方位
........
.flutter {
  position: absolute;
  z-index: 9999;
  pointer-events: none;
}

将鼠标的坐标设置为克隆元素的肯定定位值(lefttop),就会像下图所示这样,此刻减去 offset 偏移量,就能让克隆元素掩盖在本体上面。

原生拖拽太拉跨了,纯JS自己手写一个拖拽作用,纵享丝滑

初始化的值需求记载起来便利后续核算,同时咱们用 dragging 变量符号了状况(拖动中),接下来合作移动鼠标的监听事情就能将元素“抓”起来了:

// 鼠标移动
window.addEventListener("mousemove", (e) => {
  if (dragging && cloneEl) {
    // TODO: 处理元素的移动:改变 left top 定位
    // x 轴(left)核算办法:e.clientX - initial.offsetX
    // y 轴(top)核算办法:e.clientY - initial.offsetY
  }
})
原生拖拽太拉跨了,纯JS自己手写一个拖拽作用,纵享丝滑

上面仅仅完成了元素的拖动,可是”克隆“的作用实在太显着了,为了让元素看起来更像是拖出来的而不是复制出来的,咱们还要让本体隐藏,同时DOM结构不能丢失,这时只需在按下拖动时给本体元素设置个 opacity: 0,完毕时再改回透明度1就能搞定。

原生拖拽太拉跨了,纯JS自己手写一个拖拽作用,纵享丝滑

虽然到这功用就算完成了,但实践作用仍是有点僵硬,参阅稿定规划中的元素铺开时会固定回到一个方位,然后再收回去,这个过渡又有点鬼畜,不够流畅。其实只需让元素回退进程有一个自然地动画就行,transition 就能完成:

.is_return {
  transition: all 0.3s;
}
// 鼠标抬起
window.addEventListener("mouseup", (e) => {
  dragging = false
  if (cloneEl) {
      cloneEl.classList.add('is_return') // 加上过渡动画
      changeStyle(......) // 设置回元素的初始方位
      setTimeout(() => {
        cloneEl.remove() // 移除元素
      }, 300)
  }
})

终究我在动作完毕时给克隆元素添加了过渡特点,然后直接设置回初始坐标让克隆元素回到它的出生地点,用定时器在过渡动画继续的相同时刻后移除克隆元素,这样就有了一个平滑稳定的回退动画。

原生拖拽太拉跨了,纯JS自己手写一个拖拽作用,纵享丝滑

功能优化

因为在改变元素状况的进程中需求频繁进行多个 CSS 操作,为下降回流重绘的成本,最好将多个操作兼并起来处理,这儿运用了 cssText 来完成:

// 改变漂浮元素:x、y、缩放倍率
function moveFlutter(x, y, d = 0) {
  const scale = d ? initial.width + d < initial.fakeSize ? `transform: scale(${(initial.width + d) / initial.width})` : null : null
  const options = [`left: ${x}px`, `top: ${y}px`]
  scale && options.push(scale)
  // 将CSS处理成数组,然后丢进DOM操作办法中一次履行
  changeStyle(options)
}
// 兼并多个操作
function changeStyle(arr) {
  const original = cloneEl.style.cssText.split(';')
  original.pop()
  cloneEl.style.cssText = original.concat(arr).join(';') + ';'
}

完成拖拽扩大

扩大咱们能够运用 transform: scale 来完成,只需求将拖动方位之间的间隔当做改变系数(假设为d),那么scale改变数值即为(元素宽度 + d)/元素宽度,而扩大的终究倍数必定为 图片实践宽度/元素的宽度,只需判别不超过这个鸿沟就能够。(这个图片实践宽高在实在业务场景中主张在上传资源时就记载在数据库,这儿我是模拟的随机一个原图尺寸)。

原生拖拽太拉跨了,纯JS自己手写一个拖拽作用,纵享丝滑

两点间间隔核算公式为 (x1−x2)2+(y1−y2)2sqrt{(x_1-x_2)^2 + (y_1-y_2)^2},代码完成:

// 核算两点之间间隔
function distance({ clientX, clientY }) {
  const { clientX: x, clientY: y } = initial // 获取初始的坐标
  const b = clientX - x;
  const a = clientY - y;
  return Math.sqrt(Math.pow(b, 2) + Math.pow(a, 2))
}
window.addEventListener("mousemove", (e) => {
  if (dragging && cloneEl) {
    const d = distance(e) // 核算间隔
    moveFlutter(e.clientX - initial.offsetX, e.clientY - initial.offsetY, d)
  }
})
function moveFlutter(x, y, d = 0) {
  let scale = ''
  // 如果间隔大于0,且宽度+间隔小于实践宽度
  if( d && initial.width + d <= initial.fakeSize ) {
      scale = `transform: scale(${(initial.width + d) / initial.width})`
  }
  // TODO ... changeStyle ...
}

作用演示,GIF稍微掉帧:

原生拖拽太拉跨了,纯JS自己手写一个拖拽作用,纵享丝滑

注意元素都要设置 transform-origin: top left; 改变缩放原点到左上角,否则默认(中心为原点)的转化会产生比较显着的偏移。

完成放置

其实拖拽放置有点像是”复制”与”粘贴”,前面咱们完成了复制,放置首要就是将元素粘贴到画布当中,流程步骤如下:

  1. 如果鼠标在方针区域,复制元素到画布中,如果不在画布中,履行倒退动画
  2. 删去元素
// 完成处理
function done(x, y) {
  if (!cloneEl) { return }
  const newEl = cloneEl.cloneNode(true)
  newEl.classList.remove('flutter')
  newEl.src = cloneEl.getAttribute('raw') // 设置原图地址
  newEl.style.cssText = `left: ${x - initial.offsetX}px; top: ${y - initial.offsetY}px;`
  document.getElementById('content').appendChild(newEl)
  // TODO: 元素移除
}

判别是否在画布内抬起很简单,往画布上绑定mouseup监听事情即可,克隆的新元素有必要删去无用的特点和class,此刻设置元素的lefttop即可将元素放置进画布中,关键点在于画布内的target有可能是错的,因为如果鼠标抬起的区域现已放置了元素,那么相对偏移量就得咱们自己核算了,运用getBoundingClientRect办法获取画布自身相对于视窗的偏移,鼠标坐标减去画布自身的偏移就是元素在画布中的方位了。

原生拖拽太拉跨了,纯JS自己手写一个拖拽作用,纵享丝滑

document.getElementById('content').addEventListener("mouseup", (e) => {
  if (e.target.id !== 'content') {
    const lostX = e.x - document.getElementById('content').getBoundingClientRect().left
    const lostY = e.y - document.getElementById('content').getBoundingClientRect().top
    done(lostX, lostY)
  } else { done(e.offsetX, e.offsetY) }
})

只贴了部分关键代码,完好代码主张 在码上中检查。

鸿沟判别

如果不对鸿沟情况进行处理可能会导致拖动时产生意外的中止,无法正确回收克隆元素。

// 鼠标离开了视窗
document.addEventListener("mouseleave", (e) => {
  end()
})
// 用户可能离开了浏览器
window.onblur = () => {
  end()
}

体会优化

参阅稿定规划中元素拖拽是直接赋值原图的,原图巨细一般无法操控,免不了需求加载时刻,造成卡顿空白的问题,在网络不够快时体会特别尴尬:

原生拖拽太拉跨了,纯JS自己手写一个拖拽作用,纵享丝滑

我的优化思路是运用浏览器加载过同一张图片就会优先读缓存的机制,先用一个Image加载原图,等其加载完毕再把拖拽元素的src改成原图,这样浏览器会”主动”帮咱们优化这个进程,只需求注意一点,因为这是个异步使命,所以一定要做好对应符号,不然手速快的时分操控不好触发次序。

function simulate(url, flag) {
  cloneEl.setAttribute('raw', url)
  const image = new Image()
  image.src = url
  image.onload = function () {
    // 异步使命,克隆节点可能已不存在,flag符号是否拖动的仍是当时方针
    cloneEl && initial.flag === flag && (cloneEl.src = url)
  }
}

作用演示,故意加大了图片的分辨率差异:

完好代码

代码片段的链接在文章中会直接嵌入编辑器,能够点其右上角

原生拖拽太拉跨了,纯JS自己手写一个拖拽作用,纵享丝滑
进入更便利详细地检查代码。

下一篇文章:《原生JS手写一个优雅的图片预览功用》

往期文章引荐

# 如何编写一个高逼格的JS插件惊艳你的领导和同事? # 给写了个风趣又好玩的一键三连插件 | 仿B站作用 # 这道 JS 经典面试题不要背!今天帮你彻底搞懂它 # Vue 完成无限级树形选择器(无第三方依赖)

完毕

以上就是本篇全部内容了,感谢看到这儿,期望对你有所协助或启发!创造不易,如果觉得文章写得不错,能够点赞收藏支撑一下,也欢迎关注,我会更新更多实用的前端知识与技巧。我是茶无味de一天,等待与你一起成长~