不借助其他框架或库,仅靠原生JavaScript去达成那种流畅的拖拽效果,它呀,绝不仅仅只会在提升交互体验这件事上有作用,同时成为前端开发者对于自己基本功检验的一个试金石 。
拖拽交互的核心事件
鼠标事件乃是进行拖拽得以实现的基础支撑,mousedown事件是拖拽起始的标识,于这个时刻要记录下初始状况,mousemove事件承担着处理元素跟随鼠标产生移动状况的逻辑任务,对实时坐标予以计算,mouseup事件则表明着拖拽已然结束,需要去执行放置工作或者进行复位操作,这三个事件共同构建成了一个完备的交互闭环,其中任何一个都绝非可有可无 。
#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;
}
}
于实际编码之时,得要在mousedown里去绑定mousemove以及mouseup的监听器,且要在mouseup被触发之后,迅速地将它们移除掉。如此的做法能够防止元素在拖拽停止之后依旧“粘”在鼠标之上。事件坐标借助事件的clientX与clientY属性来获取,而这两个属性乃是相对于浏览器视口的距离。
克隆与视觉替换技巧
假若直接去移动原本的元素,那么就会对原始的布局造成破坏。更为优质的做法是,当处于mousedown这个时刻,运用cloneNode方法去复制出一个与原元素面貌完全一样的元素。接着呢,把这个克隆出来的元素的position设定为absolute,进而让它脱离文档流。随后,再把它的z-index设定为一个数值较大的情况,以此来保障它能够显示在最顶层 。
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: 初始化克隆元素的定位并记录,方便后面移动时计算位置
........
将原本元素的opacity设为0,以此营造“抓起”这种错觉,进而使其在视觉方面呈现隐藏效果,而且实施该操作还必须立刻进行这样的设置。当处于这种状态下,用户所看到并且移动的对象乃是克隆体,然而原DOM节点于结构里的位置不会发生任何变化、保持原位。诸多成熟UI库在实现方案上普遍运用这个技巧,它对视觉效果以及代码稳定性两个方面都予以了兼顾与考虑。
.flutter {
position: absolute;
z-index: 9999;
pointer-events: none;
}
精准计算元素定位
定位计算属于难点。没法简单轻易把鼠标坐标赋予给元素的left和top。由于鼠标有可能点于元素任意位置之上,直接进行赋值会致使元素左上角瞬息就跳到鼠标点处,进而产生突兀的那种位移。正确的做法是要去计算鼠标在元素内部之中的初始偏移量。
在触发mousedown这个事件的时候,去记录鼠标点相对于被克隆元素左上角的偏移量offsetX以及offsetY。之后在触发mousemove的时候,用最新的鼠标坐标减去这个初始阶段的偏移量,而所得到的结果才是克隆元素左上角应当具备的坐标。这样的计算确保了在进行拖拽的进程当中,鼠标与元素之间的相对位置始终能够维持不变。
实现放置与画布集成
// 鼠标移动
window.addEventListener("mousemove", (e) => {
if (dragging && cloneEl) {
// TODO: 处理元素的移动:改变 left top 定位
// x 轴(left)计算方法:e.clientX - initial.offsetX
// y 轴(top)计算方法:e.clientY - initial.offsetY
}
})
放置是拖拽的终点,通常页面存在像设计工具“画布”这样的目标区域,能在画布元素上借助监听mouseover事件,判定是否可以放置,也能通过监听dragover事件来进行判断,更普遍的做法是在mouseup的时候,去判断鼠标坐标是不是落在画布区域之内。
倘若确定进行放置,那就得把克隆而成的元素,从临时设定的绝对定位状态之中,融入到画布的正常流动或者绝对定位体系里。这表明要对其像position这样的样式属性予以重置,并且有可能把它当成画布的子节点添加进去。与此同时,得让之前处于隐藏状态的原初元素回复显示状态,以便为下一回的拖拽操作做好准备。
优化拖放手感与动画
基础功能成就就绪之后,手感兴许会显得生硬。参照优质设计用具,元素被释放之后常常会存有一个具备弹性回弹或者是平滑移动至目标所在位置的动画效果。这能够借助CSS的transition抑或是transform特性达成。比如说,于释放的时刻展开最终位置的计算,接着给元素增添一段为时短暂的位置变换方面的过渡举措。
再一种优化是增添视觉反馈,于拖拽进程里,能够变换鼠标指针样式,或者在画布范围展示一个半透明的放置预览框,这些细微之处可显著减轻用户认知负担,清晰传达当下系统的状态,使得操作感受更为直接、更为自然。
.is_return {
transition: all 0.3s;
}
性能提升与缓存策略
// 鼠标抬起
window.addEventListener("mouseup", (e) => {
dragging = false
if (cloneEl) {
cloneEl.classList.add('is_return') // 加上过渡动画
changeStyle(......) // 设置回元素的初始位置
setTimeout(() => {
cloneEl.remove() // 移除元素
}, 300)
}
})
在有大量元素或者包含图片的元素被拖拽之际,是需要去留意性能方面情况的,应当防止在诸如mousemove那般为高频发生的相关事件里面去开展复杂的DOM查询或者样式计算操作,凭借获取requestAnimationFrame来对视觉更新进行节制是较为通行的办法,对于图片来讲,预先加载这件事是极其关键重要的。
能够创建一个处于离屏状态的Image对象,借此提前去加载图片资源。倘若用户着手去拖拽某一个图片元素,那么克隆体能够直接运用已经缓存好的图片,以此来避免在拖拽运行过程中因网络加载而出现卡顿或者 blank 的情况。这对于增强用户所感知到的速度,特别是在移动网络的环境状况下,成效极为显著。
达成一个具备丝滑特性的拖拽组件,你觉得在其实现过程中,最为难以权衡的方面,究竟是功能所具有的完备程度呢,还是代码本身所呈现出的简洁状况以及对应具备的可进行维护的性能呢?
// 改变漂浮元素: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(';') + ';'
}