Bootstrap

微信小程序长按拖拽排序

目录

一、拖拽效果

二、拖拽组件drag-item

wxml

wxs

 js

wxss

 json

三、拖拽组件drag-to-sort

wxml

js 

json

wxss

四、使用组件

wxml

js

json

wxss


一、拖拽效果

 

二、拖拽组件drag-item

wxml

<wxs module="dragToSort" src="./index.wxs"></wxs>
<view class="drag-and-drop-item" linked="{{linked}}" change:linked="{{dragToSort.linked}}" poslist="{{posList}}" change:poslist="{{dragToSort.posListChanged}}" data-idx="{{idx}}" data-drag-direction="{{dragY ? 'dragY' : (dragX ? 'dragX' : '')}}">
    <slot name="body"></slot>
    <slot name="btn" bind:longpress="{{dragToSort.longPress}}" bind:touchmove="{{dragToSort.touchMove}}" bind:touchend="{{dragToSort.touchEnd}}" data-idx="{{idx}}" data-drag-direction="{{dragY ? 'dragY' : (dragX ? 'dragX' : '')}}" data-pos-list="{{posList}}" data-window="{{window}}"></slot>
</view>

wxs

var arrayCompare = function (property) {
    return function (a, b) {
        var value1 = a[property]
        var value2 = b[property]
        return value1 - value2
    }
}

var getBounds = function (idx, posList, dragDirection) { // 获取边界值
    var posList = posList.concat([]).sort(arrayCompare('currentIndex'))
    var bounds = []
    var sum = 0
    var pre = 0
    var center = 0
    var next = 0
    if (dragDirection == 'dragY') { // y轴可拖拽
        posList.forEach(function (item, index) {
            if (index < idx || index > idx) { // pre
                bounds.push(sum + (item.size.height / 2))
                sum += item.size.height
            } else if (index == idx) {
                pre = sum
                bounds.push(sum + (item.size.height / 2))
                sum += item.size.height
                center = bounds[bounds.length - 1]
                next = sum
            }
        })
    } else if (dragDirection == 'dragX') { // x轴可拖拽
        posList.forEach(function (item, index) {
            if (index < idx || index > idx) { // pre
                bounds.push(sum + (item.size.width / 2))
                sum += item.size.width
            } else if (index == idx) {
                pre = sum
                bounds.push(sum + (item.size.width / 2))
                sum += item.size.width
                center = bounds[bounds.length - 1]
                next = sum
            }
        })
    }
    bounds = bounds.filter(function (item) {
        return item != center
    }).map(function (item) {
        if (item > center) {
            return item - next
        } else if (item < center) {
            return item - pre
        }
    })
    return { bounds, pre, center, next }
}

var initBounds = function (e, ownerInstance) {
    var state = ownerInstance.getState()
    var window = e.currentTarget.dataset.window
    var idx = e.currentTarget.dataset.idx
    var dragDirection = e.currentTarget.dataset.dragDirection
    var currentIdx = state.posList[idx].currentIndex
    var clientX = parseInt(e.touches[0].clientX)
    var clientY = parseInt(e.touches[0].clientY)
    // 拖拽的是第几个,用于高亮显示等
    ownerInstance.addClass('dragging')
    ownerInstance.callMethod('log', {event: 'initBounds', ownerInstance: ownerInstance, e: e})
    var wxsStyle = {}
    if (dragDirection == 'dragY') {
        wxsStyle = {'-webkit-transform': 'translateY(' + state.posList[idx].translate.y + 'px)', 'transform': 'translateY(' + state.posList[idx].translate.y + 'px)', 'zIndex': '99'}
    } else if (dragDirection == 'dragX') {
        wxsStyle = {'-webkit-transform': 'translateX(' + state.posList[idx].translate.x + 'px)', 'transform': 'translateX(' + state.posList[idx].translate.x + 'px)', 'zIndex': '99'}
    }
    
    ownerInstance.setStyle(wxsStyle)
    // 记录安全区域
    state.window = window
    // 拖拽起始点坐标(clientX, clientY)
    state.lastPoint = {x: clientX, y: clientY}
    state.dragStartOffset = { x: clientX, y: clientY }
    state.offsetRect = {top: 20, right: state.window.width - 20, bottom: state.window.height - 20, left: 20}
    // 拖拽的元素,记录下此时的数据,在拖拽过程中使用,此处要深拷贝
    state.dragEle = JSON.parse(JSON.stringify(state.posList[idx]))
    // 基于拖拽的元素当前的排序,获取边界信息,并获取到前一个/后一个的边界值(即超过该边界值时,排序会发生变化)
    var boundResult = getBounds(currentIdx, state.posList, dragDirection)
    state.bounds = boundResult.bounds
    state.boundaryPre = state.bounds[currentIdx - 1]
    state.boundaryNext = state.bounds[currentIdx]
    // 初始化拖拽元素最终的偏移量
    state.dragEndDistance = 0
    // 初始化拖拽元素所在的排序位置(实时变化)为undefined
    state._currentIdx = undefined
    // 记录当前排序,用于拖拽结束后对比排序,决定是否触发change事件
    var _posList = JSON.parse(JSON.stringify(state.posList))
    state._sortArr = _posList.sort(arrayCompare('currentIndex')).map(function (item) {
        return item.originalIndex
    })
    ownerInstance.callMethod('getScrollerRect')
}


module.exports = {
    posListChanged: function (newVal, oldVal, ownerInstance, instance) {
        var state = ownerInstance.getState()
        if (!oldVal && newVal) {
            state.transitionStyle = ownerInstance.getComputedStyle(['-webkit-transition', 'transition'])
        }
        if (newVal && !state.dragging && (state.idx || state.idx === 0) && state.dragElement) {
            ownerInstance.removeClass('dragging')
            ownerInstance.setStyle({'-webkit-transition': state.transitionStyle ? (state.transitionStyle['-webkit-transition'] + ', transform 0.3s') : 'transform 0.3s', 'transition': state.transitionStyle ? (state.transitionStyle['transition'] + ', transform 0.3s') : 'transform 0.3s', 'transform': 'translateY(' + newVal[state.idx].translate.y + 'px)', transform: 'translateY(' + newVal[state.idx].translate.y + 'px)', 'z-index': '1'})
        }
    },
    linked: function (newVal, oldVal, ownerInstance, instance) {
        var state = ownerInstance.getState()
        if (newVal) {
            state.dragElement = ownerInstance.selectComponent('.drag-and-drop-item')
            var rect = ownerInstance.getBoundingClientRect()
            var dataset = state.dragElement.getDataset()
            ownerInstance.callMethod('updatePosList', {rect, dataset})
            state.idx = dataset.idx
        }
    },
    longPress: function (e, ownerInstance) {
        var state = ownerInstance.getState()
        state.dragElement = ownerInstance.selectComponent('.drag-and-drop-item')
        state.posList = e.currentTarget.dataset.posList
        state.dragging = true
        if (state.posList && state.posList.length > 0) {
            initBounds(e, ownerInstance)
        }
    },
    touchMove: function (e, ownerInstance) {
        var state = ownerInstance.getState()
        if (!state.dragStartOffset) { // 起始点坐标不存在则认为没有进行拖拽操作,在拖拽结束的时候需要重置为undefined
            return
        }
        var dragDirection = e.currentTarget.dataset.dragDirection
        var clientX = parseInt(e.touches[0].clientX)
        var clientY = parseInt(e.touches[0].clientY)
        // 拖拽元素相对于起始点的偏移坐标
        state.point = {x: clientX, y: clientY}
        var distanceOffset = { x: clientX - state.dragStartOffset.x, y: clientY - state.dragStartOffset.y }
        // 初始化拖拽元素即将达到的排序
        var willIndex = undefined
        // 初始化纵向/横向的拖拽距离int
        var distance = 0
        // 拖拽过程中,拖拽元素所在的排序位置(实时变化)
        state._currentIdx = state._currentIdx == undefined ? state.dragEle.currentIndex : state._currentIdx
        // 拖拽元素的初始排序(排序列表初始化时的排序)
        var originalIndex = state.dragEle.originalIndex
        // 判断拖拽方向
        // var direction = ''
        // var outOffArea = ''
        // if (dragDirection == 'dragY') {
        //     direction = state.point.y - state.lastPoint.y > 0 ? 'y' : '-y'
        //     outOffArea = (state.point.y - state.posList[originalIndex].size.height / 2 < state.offsetRect.top) || (state.point.y + state.posList[originalIndex].size.height / 2 > state.offsetRect.bottom)
        // } else if (dragDirection == 'dragX') {
        //     direction = state.point.x - state.lastPoint.x > 0 ? 'x' : '-x'
        //     outOffArea = (state.point.x - state.posList[originalIndex].size.width / 2 < state.offsetRect.left) || (state.point.x + state.posList[originalIndex].size.width / 2 > state.offsetRect.right)
        // }
        // if (outOffArea) {

        // }
        // // 实时更新拖拽元素的位置
        if (dragDirection == 'dragY') {
            state.posList[originalIndex].translate.y = state.dragEle.translate.y + distanceOffset.y
            ownerInstance.setStyle({'-webkit-transition': 'none', 'transition': 'none', '-webkit-transform': 'translateY(' + state.posList[originalIndex].translate.y + 'px)', 'transform': 'translateY(' + state.posList[originalIndex].translate.y + 'px)', 'z-index': '99'})
            distance = distanceOffset.y
        } else if (dragDirection == 'dragX') {
            state.posList[originalIndex].translate.x = state.dragEle.translate.x + distanceOffset.x
            ownerInstance.setStyle({'-webkit-transition': 'none', 'transition': 'none', '-webkit-transform': 'translateX(' + state.posList[originalIndex].translate.x + 'px)', 'transform': 'translateX(' + state.posList[originalIndex].translate.x + 'px)', 'z-index': '99'})
            distance = distanceOffset.x
        }

        if (!state.currentIndexChanging) { // 当拖拽元素在设置时不执行,避免太过频繁的更新数据导致卡顿
            // 前一个的边界值存在 且 距离超过该边界时,认为排序向前移动
            if (state.boundaryPre !== undefined && distance < state.boundaryPre) {
                // 更新排序,并获取拖拽元素所取代元素的初始排序(排序列表初始化时的排序)
                willIndex = state._currentIdx - 1
                var preOriginalIndex = state.posList.filter(function (item) {
                    return item.currentIndex === willIndex
                })[0].originalIndex
                // 更新currentIndex字段,并更改拖拽元素所取代元素的偏移
                if (dragDirection == 'dragY') {
                    state.posList[preOriginalIndex].translate.y = state.posList[preOriginalIndex].translate.y + state.dragEle.size.height
                } else if (dragDirection == 'dragX') {
                    state.posList[preOriginalIndex].translate.x = state.posList[preOriginalIndex].translate.x + state.dragEle.size.width
                }
                state.posList[preOriginalIndex].currentIndex = parseInt(state._currentIdx)
                state.posList[originalIndex].currentIndex = parseInt(willIndex)
                ownerInstance.callMethod('updatePos', {pos: state.posList[preOriginalIndex], updateIndex: preOriginalIndex, oldPos: state._currentIdx, newPos: willIndex})

                // 更新操作数据
                state._currentIdx = state._currentIdx - 1
                if (dragDirection == 'dragY') {
                    state.dragEndDistance -= state.posList[preOriginalIndex].size.height
                } else if (dragDirection == 'dragX') {
                    state.dragEndDistance -= state.posList[preOriginalIndex].size.width
                }
                
                // 当排序有变化时,锁定,直到界面更新渲染完毕后取消锁定
                state.currentIndexChanging = true
            } else if (state.boundaryNext !== undefined && distance > state.boundaryNext) {
                // 更新排序,并获取拖拽元素所取代元素的初始排序(排序列表初始化时的排序)
                willIndex = state._currentIdx + 1
                var nextOriginalIndex = state.posList.filter(function (item) {
                    return item.currentIndex === willIndex
                })[0].originalIndex
                // 更新currentIndex字段,并更改拖拽元素所取代元素的偏移
                if (dragDirection == 'dragY') {
                    state.posList[nextOriginalIndex].translate.y = state.posList[nextOriginalIndex].translate.y - state.dragEle.size.height
                } else if (dragDirection == 'dragX') {
                    state.posList[nextOriginalIndex].translate.x = state.posList[nextOriginalIndex].translate.x - state.dragEle.size.width
                }
                state.posList[nextOriginalIndex].currentIndex = parseInt(state._currentIdx)
                state.posList[originalIndex].currentIndex = parseInt(willIndex)
                ownerInstance.callMethod('updatePos', {pos: state.posList[nextOriginalIndex], updateIndex: nextOriginalIndex, oldPos: state._currentIdx, newPos: willIndex})
                
                // 更新操作数据
                state._currentIdx = state._currentIdx + 1
                if (dragDirection == 'dragY') {
                    state.dragEndDistance += state.posList[nextOriginalIndex].size.height
                } else if (dragDirection == 'dragX') {
                    state.dragEndDistance += state.posList[nextOriginalIndex].size.width
                }
                
                // 当排序有变化时,锁定,直到界面更新渲染完毕后取消锁定
                state.currentIndexChanging = true
            }
            // 如果排序有变化,则更新前一个/后一个边界值
            if (state.currentIndexChanging) {
                state.boundaryPre = state.bounds[state._currentIdx - 1]
                state.boundaryNext = state.bounds[state._currentIdx]
            }
        }

        state.lastPoint = {x: clientX, y: clientY}
        state.currentIndexChanging = false
        if (state.dragging) {
            return false
        }
    },
    touchEnd: function (e, ownerInstance) {
        var state = ownerInstance.getState()
        if (!state.dragStartOffset) {
            return
        }
        var dragDirection = e.currentTarget.dataset.dragDirection
        // 清除拖拽起始点坐标,仅在长按后可获得起始点坐标
        state.dragStartOffset = undefined
        state.dragging = false
        
        if (state.dragEle) {
            var originalIndex = state.dragEle.originalIndex
            ownerInstance.removeClass('dragging')
            // ownerInstance.removeClass('item-dragging')
            // 根据最终拖拽元素的偏移,更新拖拽元素的位置
            if (dragDirection == 'dragY') {
                state.posList[originalIndex].translate.y = state.dragEle.translate.y + state.dragEndDistance
                ownerInstance.setStyle({'-webkit-transition': state.transitionStyle ? (state.transitionStyle['-webkit-transition'] + ', transform 0.3s') : 'transform 0.3s', 'transition': state.transitionStyle ? (state.transitionStyle['transition'] + ', transform 0.3s') : 'transform 0.3s', '-webkit-transform': 'translateY(' + state.posList[originalIndex].translate.y + 'px)', 'transform': 'translateY(' + state.posList[originalIndex].translate.y + 'px)', 'z-index': '1'})
            } else if (dragDirection == 'dragX') {
                state.posList[originalIndex].translate.x = state.dragEle.translate.x + state.dragEndDistance
                ownerInstance.setStyle({'-webkit-transition': state.transitionStyle ? (state.transitionStyle['-webkit-transition'] + ', transform 0.3s') : 'transform 0.3s', 'transition': state.transitionStyle ? (state.transitionStyle['transition'] + ', transform 0.3s') : 'transform 0.3s', '-webkit-transform': 'translateX(' + state.posList[originalIndex].translate.x + 'px)', 'transform': 'translateX(' + state.posList[originalIndex].translate.x + 'px)', 'z-index': '1'})
            }
            ownerInstance.callMethod('updatePos', {pos: state.posList[originalIndex]})
        }

        var _posList = JSON.parse(JSON.stringify(state.posList))
        var sortArr = _posList.sort(arrayCompare('currentIndex')).map(function (item) {
            return item.originalIndex
        })
        if (state._sortArr && JSON.stringify(state._sortArr) != JSON.stringify(sortArr)) { // 排序改变,触发change事件,否则不触发
            ownerInstance.callMethod('parentChange', { sort: sortArr })
        }
        state._sortArr = undefined
        
    }
}

 js

Component({
    relations: {
        '../drag-to-sort/index': {
            type: 'parent', // 关联的目标节点应为父节点
            linked: function(target) {
                const {dragY, dragX} = target.data
                const systemInfo = wx.getSystemInfoSync()
                this.parent = target
                this.setData({ dragY, dragX, window: {width: systemInfo.windowWidth, height: systemInfo.windowHeight}, linked: true })
                this.updatePosList = data => {
                    const {rect, dataset} = data
                    target.posList = target.posList || []
                    target.posList.push({originalIndex: dataset.idx, currentIndex: dataset.idx, size: {width: rect.width, height: rect.height}, translate: {x: 0, y: 0}})
                    target.posListLen = target.posListLen || 0
                    target.posListLen += 1
                    if (target.posListLen == target.data.length) {
                        target.posList = target.posList.sort(this.arrayCompare('currentIndex'))
                        target.allChildLinked(target.posList)
                    }
                }
            }
        }
    },
    options: {
        multipleSlots: true // 在组件定义时的选项中启用多slot支持
    },
    properties: {
        idx: {
            type: Number,
            value: 0
        }
    },
    data: {
        
    },
    methods: {
        getScrollerRect() {
            return
            this.parent.getScrollerRect()
        },
        updatePos({pos, updateIndex, oldPos, newPos}) {
            console.log('updatePos', pos, updateIndex, oldPos, newPos)
            let posList = this.parent.posList.map(item => {
                if (item.originalIndex == pos.originalIndex) {
                    return pos
                } else {
                    return item
                }
            })
            this.parent.posList = posList
            if (updateIndex !== undefined) {
                this.parent.noticeChildInited(updateIndex)
            } else {
                this.parent.noticeChildInited()
            }
            if (oldPos != newPos) {
                this.parentPosChange({oldPos, newPos})
            }
        },
        updatePosDate(posList) {
            this.setData({posList})
        },
        arrayCompare (property) {
            return function (a, b) {
                var value1 = a[property]
                var value2 = b[property]
                return value1 - value2
            }
        },
        log(data) {
            console.log(data)
        },
        parentChange(data) {
            this.parent.change(data)
        },
        parentPosChange(data) {
            this.parent.posChange(data)
        }
    }
})

wxss

.drag-and-drop-item{
    word-break: break-all;
    position: relative;
    z-index: 1;
    display: inherit;
    justify-content: inherit;
    align-items: inherit;
    flex-direction: inherit;
    flex-wrap: inherit;
    flex: 1;
}
:host{
    position: relative;
}

 json

{
    "component": true
}

 

三、拖拽组件drag-to-sort

wxml

<view class="drag-and-drop-list{{dragY ? ' drag-y' : ''}}{{dragX ? ' drag-x' : ''}}" data-drag-direction="{{dragY ? 'dragY' : (dragX ? 'dragX' : '')}}">
    <slot></slot>
</view>

js 

Component({
    relations: {
        '../drag-item/index': {
          type: 'child', // 关联的目标节点应为子节点
          linked: function(target) {
            this.child = this.child || []
            this.child.push(target)
          }
        }
    },
    properties: {
        dragY: {
            type: Boolean,
            value: false
        },
        dragX: {
            type: Boolean,
            value: false
        },
        length: {
            type: Number,
            value: 0
        }
    },
    data: {
        allLoaded: false
    },
    methods: {
        getScrollerRect() {
            const _query = this.createSelectorQuery()
            const query = wx.createSelectorQuery()
            _query.select('.drag-and-drop-list').boundingClientRect().exec(res => {
                console.log('_query_res', res)
            })
            query.select('#scroller').fields({
                size: true,
                scrollOffset: true,
                properties: ['scrollX', 'scrollY'],
                context: true,
              }).exec(res => {
                console.log('query_res', res)
            })
        },
        noticeChildInited(index) { // 如果index不为undefined,着仅更新index指定的子元素,否则认为更新全部元素
            if (this.child && this.posList) {
                this.child.forEach(child => {
                    if (index === undefined || (index !== undefined && child.data.idx == index)) {
                        child.updatePosDate(this.posList)
                    }
                })
            }
        },
        log(data) {
            console.log(data)
        },
        allChildLinked(posList) {
            const pages = getCurrentPages()
            const lastPage = pages[pages.length - 1]
            setTimeout(() => {
                const query = wx.createSelectorQuery()
                query.select('#scroller').scrollOffset().exec((res) => {
                    console.log('rect', res)
                })
            }, 3000)
            

            // console.log('rect', rect)
            let _posList = []
            let height = 0
            let width = 0
            posList.forEach(item => {
                height += item.size.height
                width += item.size.width
            })
            
            let spaceTop = 0
            let spaceLeft = 0
            
            _posList = posList.map(item => {
                item.space = {top: spaceTop, right: width - spaceLeft - item.size.width, bottom: height - spaceTop - item.size.height, left: spaceLeft}
                if (this.data.dragY) {
                    item.space.left = null
                    item.space.right = null
                } else if (this.data.dragX) {
                    item.space.top = null
                    item.space.bottom = null
                }
                spaceTop += item.size.height
                spaceLeft += item.size.width
                return item
            })
            this.posList = _posList
            console.log('posList', this.posList)
            this.noticeChildInited()
        },
        change(data) {
            this.triggerEvent('change', data)
        },
        posChange(data) {
            // console.log('posChange', data)
            this.triggerEvent('poschange', data)
        }
    }
})

json

{
    "component": true
}

wxss

.wrapper{
    position: relative;
}
.drag-and-drop-list{
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: stretch;
    position: relative;
  }
  .drag-and-drop-list.drag-y{
    flex-direction: column;
  }
.drag-and-drop-list.drag-x{
flex-direction: row;
}
.drag-and-drop-item{
    word-break: break-all;
    position: relative;
    z-index: 1;
}

 

四、使用组件

wxml

<!--index.wxml-->
<view class="container">
  <scroll-view id="scroller" scroll-y>
    <drag-to-sort length="{{list.length}}" drag-y bind:change="sortChange" bind:poschange="posChange">
      <drag-item wx:for="{{list}}" wx:key="*this" class="item" idx="{{index}}">
        <view slot="body" class="body">{{item}}</view>
        <view slot="btn" class="btn">
          <view class="btn-sign"></view>
        </view>
      </drag-item>
    </drag-to-sort>
  </scroll-view>
</view>

js

Page({
  data: {
    list: ['测试11111', '测试22222', '测试33333', '测试444444', '测试555555', '测试66666', '测试77777', '测试88888888', '测试9999999999999', '101010101010', '20202020202002020','3030303030303030', '404040404040404040', '5050505050505050', '6060606060606060606', '70707070707070707070'],
  },
  sortChange(e) {
    console.log('sortChange', e)
  },
  posChange(e) {
    // console.log('posChange', e)
    // wx.vibrateShort({
    //   type: 'light'
    // })
  }
})

json

{
  "usingComponents": {
    "drag-to-sort": "/components/drag-to-sort/index",
    "drag-item": "/components/drag-item/index"
  }
}

wxss

/**index.wxss**/
#scroller{
  position: fixed;
  width: 100%;
  height: 100%;
  left: 0;
  top: 0;
}
.item{
  min-height: 80rpx;
  font-size: 32rpx;
  line-height: 56rpx;
  display: flex;
  justify-content: space-between;
  align-items: center;
  word-break: break-all;
  position: relative;
  padding: 0 0 0 20px;
  transition: all 0.3s;
}
.item.dragging{
  background: pink;
  color: yellow
}
.body{
  flex: 1;
}
.btn{
  padding: 10px 20px 10px 20px;
}
.btn .btn-sign, .btn:before, .btn:after{
  display: block;
  content: " ";
  margin: 4px 0;
  width: 40rpx;
  height: 2px;
  background: #999999;
  border-radius: 2px;
}

原作者链接:小程序实现列表拖拽排序 | 微信开放社区 

;