533 lines
16 KiB
Vue
533 lines
16 KiB
Vue
<template>
|
||
<view class="l-drag l-class" :style="[areaStyles]" ref="dragRef" @touchstart="setDisabled">
|
||
<movable-area class="l-drag__inner" v-if="isReset" :style="[innerStyles]">
|
||
<slot></slot>
|
||
<movable-view class="l-drag__ghost" v-if="isDrag && props.ghost" :animation="true" :style="[viewStyles]" direction="all" :x="ghostEl.x" :y="ghostEl.y" key="l-drag-clone">
|
||
<slot name="ghost"></slot>
|
||
</movable-view>
|
||
<movable-view v-if="props.before" class="l-drag__before" disabled :animation="false" :style="[viewStyles]" :x="beforeEl.x" :y="beforeEl.y">
|
||
<slot name="before"></slot>
|
||
</movable-view>
|
||
<movable-view
|
||
v-for="(item, oindex) in cloneList" :key="item.id"
|
||
direction="all"
|
||
:data-oindex="oindex"
|
||
:style="[viewStyles]"
|
||
class="l-drag__view"
|
||
:class="[{'l-is-active': oindex == active, 'l-is-hidden': !item.show}, item.class]"
|
||
:x="item.x"
|
||
:y="item.y"
|
||
:friction="friction"
|
||
:damping="damping"
|
||
:animation="animation"
|
||
:disabled="isDisabled || props.disabled"
|
||
@touchstart="touchStart"
|
||
@change="touchMove"
|
||
@touchend="touchEnd"
|
||
@touchcancel="touchEnd"
|
||
@longpress="setDisabled"
|
||
>
|
||
<!-- <view v-if="props.remove" class="l-drag__remove" :style="removeStyle" data-remove="true">
|
||
<slot name="remove" :oindex="oindex" data-remove="true" />
|
||
</view> -->
|
||
<!-- <view v-if="props.handle" class="l-drag__handle" :style="handleStyle" data-handle="true">
|
||
<slot name="handle" :oindex="oindex" :active="!isDisabled && !isDisabled && oindex == active" />
|
||
</view> -->
|
||
<slot name="grid" :oindex="oindex" :index="item.index" :oldindex="item.oldindex" :content="item.content" :active="!isDisabled && !isDisabled && oindex == active" />
|
||
<view class="mask" v-if="!(isDisabled || props.disabled) && props.longpress"></view>
|
||
</movable-view>
|
||
|
||
|
||
<movable-view v-if="props.after" class="l-drag__after" disabled :animation="true" direction="all" :style="[viewStyles]" :x="afterEl.x" :y="afterEl.y">
|
||
<slot name="after"></slot>
|
||
</movable-view>
|
||
</movable-area>
|
||
</view>
|
||
</template>
|
||
<script lang="ts">
|
||
// @ts-nocheck
|
||
import { computed, onMounted, ref, getCurrentInstance, watch, nextTick, reactive , triggerRef, onUnmounted, defineComponent} from "./vue";
|
||
import DragProps from './props';
|
||
import type {GridRect, Grid, Position} from './type'
|
||
// #ifdef APP-NVUE
|
||
const dom = weex.requireModule('dom')
|
||
// #endif
|
||
|
||
export default defineComponent({
|
||
name: 'l-drag',
|
||
externalClasses: ['l-class'],
|
||
options: {
|
||
addGlobalClass: true,
|
||
virtualHost: true,
|
||
},
|
||
props: DragProps,
|
||
emits: ['change'],
|
||
setup(props, {emit, expose}) {
|
||
const res = wx.getSystemInfoSync();
|
||
const statusHeight = res.statusBarHeight; //状态栏高度
|
||
const cusnavbarheight = (statusHeight + 44) + "px";
|
||
// #ifdef APP-NVUE
|
||
const dragRef = ref(null)
|
||
// #endif
|
||
const app = getCurrentInstance()
|
||
const isDrag = ref(false)
|
||
const isInit = ref(false)
|
||
const isReset = ref(true)
|
||
const colmunId = ref(-1)
|
||
/** 选中项原始下标 */
|
||
const active = ref(-1)
|
||
const maxIndex = ref(-1)
|
||
const animation = ref(true)
|
||
const isDisabled = ref(props.handle || props.longpress ? true: false)
|
||
|
||
const dragEl = reactive({
|
||
content: null,
|
||
/** 当前视图下标*/
|
||
index: 0,
|
||
/** 旧视图下标 */
|
||
oldindex: -1,
|
||
/** 上次原始下标 */
|
||
lastindex: -1
|
||
})
|
||
|
||
const ghostEl = reactive({
|
||
content: null,
|
||
x: 0,
|
||
y: 0
|
||
})
|
||
const beforeEl = reactive({
|
||
x: 0,
|
||
y: 0
|
||
})
|
||
const afterEl = reactive({
|
||
x: 0,
|
||
y: 0
|
||
})
|
||
|
||
let gridRects = [] //ref<GridRect[]>([])
|
||
const areaWidth = ref(0)
|
||
const cloneList = ref<Grid[]>([])
|
||
// 删除项时可能会减少行数影响到删除过渡动画,故增加此值在删除时保持高度不变,等动画完成后再归零
|
||
const leaveRow = ref(0)
|
||
const extra = computed(() => (props.before ? 1 :0) + (props.after ? 1 : 0))
|
||
const rows = computed(() => Math.ceil( ((isInit.value ? cloneList.value.length : props.list.length) + extra.value) / props.column ))
|
||
const gridHeight = computed(() => props.aspectRatio ? girdWidth.value / props.aspectRatio : (/rpx$/.test(`${props.gridHeight}`) ? uni.upx2px(parseInt(`${props.gridHeight}`)) : parseInt(`${props.gridHeight}`)))
|
||
const girdWidth = computed(() => areaWidth.value / props.column)
|
||
const viewStyles = computed(() => ({width: girdWidth.value + 'px',height: gridHeight.value + 'px'}))
|
||
const areaStyles = computed(() => ({height: (rows.value + leaveRow.value ) * gridHeight.value + 'px'}))
|
||
const innerStyles = computed(() => ({
|
||
// #ifdef APP-NVUE
|
||
width: areaWidth.value + 'px',
|
||
// #endif
|
||
height: (rows.value + props.extraRow + leaveRow.value) * gridHeight.value + 'px'}))
|
||
|
||
const sleep = (cb: Function, time = 1000/60) => setTimeout(cb, time)
|
||
const createGrid = (content: any, position?:Position|null): Grid => {
|
||
colmunId.value++
|
||
maxIndex.value++
|
||
const index = maxIndex.value
|
||
const colmun = gridRects[index]
|
||
|
||
let x = 0
|
||
let y = 0
|
||
if(colmun) {
|
||
if(props.after) {
|
||
let nxet = gridRects[index + 1]
|
||
if(!nxet) {
|
||
nxet = createGridRect(gridRects.length + (props.before ? 1 : 0))
|
||
gridRects.push(nxet)
|
||
}
|
||
setReset(() => setAfter(nxet))
|
||
} else {
|
||
setReset()
|
||
}
|
||
x = colmun.x
|
||
y = colmun.y
|
||
} else {
|
||
const nxet = createGridRect(gridRects.length + (props.before ? 1 : 0))
|
||
gridRects.push(nxet)
|
||
setReset()
|
||
x = nxet.x
|
||
y = nxet.y
|
||
}
|
||
if(position) {
|
||
x = position.x
|
||
y = position.y
|
||
}
|
||
return {id: `l-drag-item-${colmunId.value}`, index, oldindex: index, content, x, y, class: '', show: true}
|
||
}
|
||
const setReset = (cb?: any) => {
|
||
// const newRow = (cloneList.value.length + extra.value) % (props.column)
|
||
if(isInit.value) {
|
||
cb&&sleep(cb)
|
||
}
|
||
}
|
||
const setAfter = ({x, y} = {x: 0, y: 0}) => {
|
||
if(props.after) {
|
||
afterEl.x = x
|
||
afterEl.y = y
|
||
}
|
||
}
|
||
const setDisabled = (e: any, flag?: boolean= false) => {
|
||
// e?.preventDefault()
|
||
const type = `${e.type}`.toLowerCase()
|
||
const {handle = props.touchHandle} = e.target.dataset
|
||
if(props.handle && !handle) {
|
||
isDisabled.value = true
|
||
} else if(props.handle && handle && !props.longpress) {
|
||
isDisabled.value = flag
|
||
} else if(props.handle && handle && props.longpress && type.includes('longpress')) {
|
||
isDisabled.value = false
|
||
} else if(props.longpress && type.includes('longpress') && !props.handle) {
|
||
isDisabled.value = false
|
||
}
|
||
if(type.includes('touchend') && props.longpress) {
|
||
isDisabled.value = true
|
||
}
|
||
}
|
||
const createGridRect = (i: number, last?: GridRect): GridRect => {
|
||
let { row } = last || gridRects[gridRects.length - 1] || { row: 0 }
|
||
const col = i % (props.column)
|
||
const grid = (row: number, x: number, y: number):GridRect => {
|
||
return {row, x, y, x1: x + girdWidth.value, y1: y + gridHeight.value}
|
||
}
|
||
if(col == 0 && i != 0) {row++}
|
||
return grid(row, col * girdWidth.value, row * gridHeight.value)
|
||
}
|
||
const createGridRects = () => {
|
||
let rects: GridRect[] = []
|
||
const length = rows.value * props.column + extra.value
|
||
gridRects = []
|
||
for (var i = 0; i < length; i++) {
|
||
const item = createGridRect(i, rects[rects.length - 1])
|
||
rects.push(item)
|
||
}
|
||
if(props.before) {
|
||
const {x, y} = rects.shift()
|
||
beforeEl.x = x
|
||
beforeEl.y = y
|
||
}
|
||
setAfter(rects[props.list.length])
|
||
gridRects = rects as GridRect[]
|
||
}
|
||
const updateList = (v: any[]) => {
|
||
cloneList.value = v.map((content) => createGrid(content))
|
||
}
|
||
|
||
const touchStart = (e: any) => {
|
||
if(e.target.dataset.remove) return
|
||
// 选中项原始下标
|
||
const {oindex} = e.currentTarget?.dataset || e.target?.dataset || {}
|
||
if(typeof oindex !== 'number') return
|
||
const target = cloneList.value[oindex]
|
||
isDrag.value = true
|
||
// 选中项原始下标
|
||
active.value = oindex
|
||
// 选中项的当前下标
|
||
dragEl.index = dragEl.oldindex = target.index
|
||
ghostEl.x = target.x||0
|
||
ghostEl.y = target.y||0
|
||
dragEl.content = ghostEl.content = target.content
|
||
}
|
||
|
||
const touchEnd = (e: any) => {
|
||
setTimeout(() => {
|
||
if(e.target.dataset.remove || active.value==-1) return
|
||
setDisabled(e, true)
|
||
isDrag.value = false
|
||
const isEmit = dragEl.index !== dragEl.oldindex && dragEl.oldindex > -1 // active.value !== dragEl.index
|
||
dragEl.lastindex = active.value
|
||
dragEl.oldindex = active.value = -1
|
||
const last = cloneList.value[dragEl.lastindex]
|
||
const position = gridRects[dragEl.index]
|
||
nextTick(() => {
|
||
last.x = position.x + 0.001
|
||
last.y = position.y + 0.001
|
||
sleep(() => {
|
||
last.x = position.x
|
||
last.y = position.y
|
||
isEmit && emitting()
|
||
})
|
||
})
|
||
},80)
|
||
|
||
}
|
||
const emitting = () => {
|
||
const clone = [...cloneList.value].sort((a, b) => a.index - b.index)//.map(item => ref(item.content))
|
||
emit('change', clone)
|
||
}
|
||
|
||
const touchMove = (e: any) => {
|
||
if(!isDrag.value) return
|
||
// #ifndef APP-NVUE
|
||
let {oindex} = e.currentTarget.dataset
|
||
// #endif
|
||
// #ifdef APP-NVUE
|
||
oindex = e.currentTarget.dataset['-oindex']
|
||
// #endif
|
||
if(oindex != active.value) return
|
||
const {x, y} = e.detail
|
||
const centerX = x + girdWidth.value / 2
|
||
const centerY = y + gridHeight.value / 2
|
||
for (let i = 0; i < cloneList.value.length; i++) {
|
||
const item = gridRects[i]
|
||
if(centerX > item.x && centerX < item.x1 && centerY > item.y && centerY < item.y1) {
|
||
ghostEl.x = item.x
|
||
ghostEl.y = item.y
|
||
if(dragEl.index != i) {
|
||
_move(active.value, i)
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
const getDragEl = (oindex: number) => {
|
||
if(isDrag.value) {return dragEl}
|
||
return cloneList.value[oindex]
|
||
}
|
||
|
||
/**
|
||
* 把原始数据中排序为index的项 移动到 toIndex
|
||
* @param oindex 原始数据的下标
|
||
* @param toIndex 视图中的下标
|
||
* @param position 指定坐标
|
||
*/
|
||
const _move = (oindex: number, toIndex: number, position?: Position|null, emit: boolean = true) => {
|
||
const length = cloneList.value.length - 1
|
||
if(toIndex > length || toIndex < 0) return
|
||
// 获取oIdnex在视图中的项目
|
||
const dragEl = getDragEl(oindex)
|
||
let speed = 0
|
||
let start = dragEl.index
|
||
// 比较开始index和终点index,设置方向
|
||
if(start < toIndex) {speed = 1}
|
||
if(start > toIndex) {speed = -1}
|
||
if(!speed) return
|
||
// 距离
|
||
let distance = start - toIndex
|
||
// 找到区间所有的项
|
||
while(distance) {
|
||
distance += speed
|
||
// 目标
|
||
const target = isDrag.value ? (dragEl.index += speed) : (start += speed)
|
||
let targetOindex = cloneList.value.findIndex(item => item.index == target && item.content != dragEl.content)
|
||
if (targetOindex == oindex) return
|
||
if (targetOindex < 0) {targetOindex = cloneList.value.length - 1}
|
||
let targetEl = cloneList.value[targetOindex]
|
||
if(!targetEl) return;
|
||
// 上一个index
|
||
const lastIndex = target - speed
|
||
const activeEl = cloneList.value[oindex]
|
||
const rect = gridRects[lastIndex]
|
||
targetEl.x = rect.x
|
||
targetEl.y = rect.y
|
||
targetEl.oldindex = targetEl.index
|
||
targetEl.index = lastIndex
|
||
activeEl.oldindex = activeEl.index //oIndex
|
||
activeEl.index = toIndex
|
||
// 到达终点,如果是拖拽则不处理
|
||
if(!distance && !isDrag.value) {
|
||
const rect = gridRects[toIndex]
|
||
const {x, y} = position||rect
|
||
activeEl.x = dragEl.x = x
|
||
activeEl.y = dragEl.y = y
|
||
// triggerRef(cloneList)
|
||
if(emit) {
|
||
emitting()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* 为区分是主动调用还是内部方法
|
||
*/
|
||
const move = (oindex: number, toIndex: number) => {
|
||
active.value = -1
|
||
isDrag.value = false
|
||
_move(oindex, toIndex)
|
||
}
|
||
// 临时处理 待有空再完善
|
||
const REMOVE_TIME = 400
|
||
let removeTimer = null
|
||
const remove = (oindex: number) => {
|
||
active.value = -1
|
||
isDrag.value = false
|
||
|
||
clearTimeout(removeTimer)
|
||
const item = cloneList.value[oindex]
|
||
if(props.disabled || !item) return
|
||
item.show = false
|
||
const after = cloneList.value.length - 1
|
||
_move(oindex, after, item, false)
|
||
setAfter(gridRects[after])
|
||
maxIndex.value--
|
||
const _remove = (_index = oindex) => {
|
||
// 小程序 删除会闪一下 所以先关闭动画再开启
|
||
// animation.value = false
|
||
const row = Math.ceil((cloneList.value.length - 1 + extra.value) / props.column)
|
||
if( row < rows.value) {
|
||
leaveRow.value = (rows.value - row)
|
||
}
|
||
cloneList.value.splice(_index, 1)[0]
|
||
emitting()
|
||
removeTimer = setTimeout(() => {
|
||
leaveRow.value = 0
|
||
}, REMOVE_TIME)
|
||
}
|
||
_remove()
|
||
}
|
||
const push = (...args: any) => {
|
||
if(props.disabled) return
|
||
if(Array.isArray(args)) {
|
||
Promise.all(args.map(async item => await add(item, true))).then(emitting)
|
||
}
|
||
}
|
||
const add = (content: any, after: boolean) => {
|
||
return new Promise((resolve) => {
|
||
const item = createGrid(content, after ? null : {x: -100, y:0})
|
||
item.class = 'l-drag-enter'
|
||
cloneList.value.push(item)
|
||
const length = cloneList.value.length - 1
|
||
nextTick(() => {
|
||
sleep(() => {
|
||
item.class = 'l-drag-leave'
|
||
_move(length, (after ? length : 0), null, false)
|
||
triggerRef(cloneList)
|
||
resolve(true)
|
||
})
|
||
})
|
||
|
||
})
|
||
}
|
||
const unshift = (...args: any) => {
|
||
if(props.disabled) return
|
||
if(Array.isArray(args)) {
|
||
Promise.all(args.map(async (item) => await add(item))).then(emitting)
|
||
}
|
||
}
|
||
|
||
// 暂时先简单处理,待有空再完善
|
||
const shift = () => {
|
||
if(!cloneList.value.length) return
|
||
remove(cloneList.value.findIndex(item => item.index == 0) || 0)
|
||
}
|
||
const pop = () => {
|
||
const length = cloneList.value.length-1
|
||
if(length < 0 ) return
|
||
remove(cloneList.value.findIndex(item => item.index == length) || length)
|
||
}
|
||
// const splice = (start, count, ...context) => {
|
||
// // 暂未实现
|
||
// }
|
||
const clear = () => {
|
||
isInit.value = isDrag.value = false
|
||
maxIndex.value = colmunId.value = active.value = -1
|
||
cloneList.value = []
|
||
gridRects = []
|
||
}
|
||
const init = () => {
|
||
clear()
|
||
createGridRects()
|
||
nextTick(() => {
|
||
updateList(props.list)
|
||
isInit.value = true
|
||
})
|
||
}
|
||
let count = 0
|
||
const getRect = () => {
|
||
count++
|
||
// #ifndef APP-NVUE
|
||
uni.createSelectorQuery().in(app.proxy).select('.l-drag').boundingClientRect((res: UniNamespace.NodeInfo) => {
|
||
if(res) {
|
||
areaWidth.value = res.width || 0
|
||
// 小程序居然无法响应式?
|
||
init()
|
||
}
|
||
}).exec()
|
||
// #endif
|
||
// #ifdef APP-NVUE
|
||
sleep(() => {
|
||
nextTick(() => {
|
||
dom.getComponentRect(dragRef.value, (res) => {
|
||
if(!res.size.width && count < 5) {
|
||
getRect()
|
||
} else {
|
||
areaWidth.value = res.size.width || 0
|
||
init()
|
||
}
|
||
})
|
||
})
|
||
})
|
||
// #endif
|
||
}
|
||
onMounted(getRect)
|
||
onUnmounted(clear)
|
||
watch(() => props.list, init)
|
||
|
||
// #ifdef VUE3
|
||
expose({
|
||
remove,
|
||
// add,
|
||
move,
|
||
push,
|
||
unshift,
|
||
shift,
|
||
pop
|
||
})
|
||
// #endif
|
||
return {
|
||
// #ifdef APP-NVUE
|
||
dragRef,
|
||
// #endif
|
||
cloneList,
|
||
|
||
areaStyles,
|
||
innerStyles,
|
||
viewStyles,
|
||
|
||
setDisabled,
|
||
isDisabled,
|
||
isReset,
|
||
isDrag,
|
||
|
||
active,
|
||
animation,
|
||
|
||
afterEl,
|
||
ghostEl,
|
||
beforeEl,
|
||
|
||
touchStart,
|
||
touchMove,
|
||
touchEnd,
|
||
|
||
remove,
|
||
// add,
|
||
move,
|
||
push,
|
||
unshift,
|
||
// shift,
|
||
// pop,
|
||
props
|
||
// isDelete: props.delete,
|
||
// ...toRefs(props)
|
||
}
|
||
}
|
||
})
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
</script>
|
||
<style lang="scss">
|
||
.l-drag{
|
||
margin-top: v-bind(cusnavbarheight);
|
||
|
||
}
|
||
@import './index';
|
||
|
||
</style>
|