造个拖拽排序的轮子

排序GIF

引子 最近遇到一个需求,要求在移动端拖拽排序.简单搜了下要么体积太大,功能太多,要么不支持移动端.轮子走起.javascript

需求示例

npm地址 git地址 在线DEMOcss

创建项目

首先按照轮子开发模板建立项目. 这个包咱们将发布到npm上,这样项目直接add就能够,方便管理.html

梳理需求细节

需求:前端

  1. 首先须要实现拖拽功能.
  2. 实现一个算法,用于断定排序.
  3. 最好同时兼容PC/移动端.

指望的使用方式:java

  1. 类的使用方式new Sorter(option)
  2. 事件回调风格sorter.on(event, callback)
  3. 没有侵入性,不须要修改太多源码.

开发

建立基类

建立一个基类,用于实现一个简单的事件回调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

要点

  1. 如何更新target 咱们知道,前端开发就是摆盒子,既然是盒子,就有四边.而咱们在移动一个盒子时,能够把这个移动盒子视做一个点,能够取鼠标的位置,也能够取盒子的中心点. 那么咱们只须要对全部盒子进行检查,移动点命中的盒子就是target 如今这个问题抽象为一个点是否在一个盒子内.
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))
复制代码
  1. dom操做性能如何? 咱们能够实现惰性操做.只在必要时进行.也就是只在target变化时取出,插入dom数组

  2. 这么多盒子,如何整理它们的数据. 以容器即父元素为参照.全部盒子的位置均按其距离父元素的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
})
复制代码

主体完整代码

Git地址

相关文章
相关标签/搜索