引子 最近遇到一個需求,要求在移動端拖拽排序.簡單搜了下要麼體積太大,功能太多,要麼不支持移動端.輪子走起.javascript
首先按照輪子開發模板建立項目. 這個包咱們將發佈到npm上,這樣項目直接add就能夠,方便管理.html
需求:前端
指望的使用方式:java
new Sorter(option)
sorter.on(event, callback)
建立一個基類,用於實現一個簡單的事件回調git
class EmitAble {
task = {}
on(event, callback) {
this.task[event] = callback
}
fire(event, payload) {
this.task[event] && this.task[event](payload)
}
}
複製代碼
簡單需求,簡單實現.有更復雜的需求能夠百度更完美的實現. 經過讓真正的功能類繼承這個基類就能夠在內部經過this.fire(event, payload)
向實例派發事件了.github
實際上瀏覽器已經爲咱們實現了一個天然排布+換行的排序規則.咱們只須要考慮將一個數組(list)中的一個元素(source)插入目標元素(target)的實現就行.算法
// 取出被拖拽元素
let temp = list.splice(source, 1);
// 截取開頭到被交換位置的元素
let start = list.splice(0, target);
// 組裝成結果數組
this.list = [...start, ...temp, ...list];
複製代碼
落到瀏覽器的實現上,就是移動一個元素(source),在移動過程當中更新target. 爲了讓這個過程能夠被看到,咱們能夠在容器中添加一個佔位元素,它的大小與被移動元素一致,而被移動元素則將其定位,使其脫離文檔流,佔位元素則根據以上算法及更新後的target反覆取出插入到新的位置.npm
isHit(point, rect) {
let {x, y} = point
let {left, top, right, bottom} = rect
return !(x < left || x > right || y < top || y > bottom)
}
let hitIndex = this.rectList.findIndex(rect => helper.isHit(point, rect))
複製代碼
dom操做性能如何? 咱們能夠實現惰性操做.只在必要時進行.也就是隻在target變化時取出,插入dom數組
這麼多盒子,如何整理它們的數據. 以容器即父元素爲參照.全部盒子的位置均按其距離父元素的top/left計算. 提供一個方法用於獲取全部子元素的位置信息.計算方式以下
getPosOfParent(el) {
let parent = el.parentNode
let pR = parent.getBoundingClientRect()
let cR = el.getBoundingClientRect()
return new Rect({
width: cR.width,
height: cR.height,
top: cR.top - pR.top,
left: cR.left - pR.left,
index: el.dataset.hasOwnProperty('index') ? +el.dataset.index : -1,
})
}
// Rect類
// 這樣拖拽元素咱們只須要更新它的top/left就能夠了.
class Rect {
constructor(opt) {
Object.assign(this, opt)
}
get centerX() {
return this.left + this.width / 2
}
get centerY() {
return this.top + this.height / 2
}
get bottom() {
return this.top + this.height
}
get right() {
return this.left + this.width
}
}
複製代碼
class Sorter extends EmitAble{
constructor(el, opt){
super()
this.$el = el
this.$options = {...initialOption, ...opt}
this.$init()
}
$init() {
this.freshThreshold()
this.listen()
}
freshThreshold() {
this.children = [...this.$el.children]
this.children.forEach((child, index) => {
child.classList.add('drag-item')
child.dataset.index = index
})
this.rectList = this.children.map(child => helper.getPosOfParent(child))
}
listen() {
this.$el.addEventListener(events.down, this.down)
this.$el.addEventListener(events.move, this.move)
document.addEventListener(events.up, this.up)
}
unbindListener(){}
down(){}
move(){}
up(){}
}
複製代碼
咱們的拖拽不須要考慮二次拖拽,比較簡單.用transform
的話,只需在move回調中,獲取與down時的鼠標間距,設置給拖拽元素便可.
down: e => {
// 點擊了可拖拽元素的子元素時,上溯到可拖拽元素
let target = this.drag = getParentByClassName(e.target, 'drag-item')
let {clientX, clientY} = e.touches ? e.touches[0] : e
let move = this.moveRect = helper.getPosOfParent(this.drag)
this.point = {
startX: clientX,
startY: clientY,
}
this.insetHolder(move.index)
}
move: e => {
e.preventDefault()
let {clientX, clientY} = e.touches ? e.touches[0] : e
let {startX, startY} = this.point
let deltaY = clientY - startY
let deltaX = clientX - startX
css(this.drag, {
transform: `translate3d(${deltaX}px,${deltaY}px,0)`,
})
}
複製代碼
但按上面提出的要點,咱們須要在down回調,使拖拽元素脫離文檔流,如設置絕對定位的話,則需將本來的位置加回來,或者設置定位時直接設置left,top. 同時,咱們須要將一個佔位元素,插入拖拽元素的位置.
insetHolder(index) {
let div = this.getHolder()
this.$el.insertBefore(div, this.$el.children[index])
}
getHolder() {
// 再次進入,則須要先從容器中取出.
if (this.$holderEl) return this.$el.removeChild(this.$holderEl)
let el = this.$holderEl = document.createElement('div')
let {width, height} = this.moveRect
el.className = 'sorter-holder'
el.style.width = width + 'px'
el.style.height = height + 'px'
el.style.background = '#f7f7f7'
return el
}
複製代碼
在move回調中,咱們還須要反映拖拽產生的效應.也就是在合適的時機,調用insertHolder.
effectSibling() {
let move = this.moveRect
let point = {
x: move.centerX,
y: move.centerY,
}
// 找到移動塊中心點進入了哪一個塊
let hitIndex = this.rectList.findIndex(rect => helper.isHit(point, rect))
if (hitIndex === -1) return
// 惰性操做.hitIndex沒有變化,什麼都不作
if (this.hidIndex === hitIndex) return
this.hidIndex = hitIndex
// 回到原位
if (hitIndex === move.index) {
this.insetHolder(move.index)
}
// 往左上移動
else if (hitIndex < move.index) {
this.insetHolder(hitIndex)
}
// 往右下移動
else {
this.insetHolder(hitIndex + 1)
}
}
複製代碼
鼠標擡起時,拖拽結束,將一切復原.咱們獲得了一個dragIndex
及一個hitIndex
. 有了這兩個值,便可按照最初的算法,真正的對元素進行排序
changeItem({source, target}) {
if (source === target) return;
const parent = this.$el;
let list = [...parent.children];
let temp = list.splice(source, 1);
let start = list.splice(0, target);
list = [...start, ...temp, ...list];
// 用fragment優化dom操做.
const frag = document.createDocumentFragment();
list.forEach(el => frag.appendChild(el));
parent.innerHTML = '';
parent.appendChild(frag);
// 刷新dragger實例
this.freshThreshold();
}
複製代碼
若是是使用MVVM框架開發,開發者每每但願獲得數據結果以後,操做數據再作更改.所以可使用fire
對外派發結果. 同時提供一個配置參數用於控制是否執行內置排序功能.
this.fire('drag-over', pos)
this.$options.change && this.changeItem(pos)
複製代碼
這樣實例就能夠經過監聽drag-over
事件綁定回調.執行一些業務邏輯.
let sorter = new Sort(el, {change: false})
sorter.on('drag-over', pos => {
console.log(pos.source, pos.target)
// do something
})
複製代碼