|
|
@@ -0,0 +1,326 @@ |
|
|
|
<template> |
|
|
|
<!-- 拖拽组件 DnD DragAndDrop --> |
|
|
|
<!-- 针对移动端draggable不生效的解决方案 父级元素定位必须为 position: relative; --> |
|
|
|
<!-- 属性 |
|
|
|
valueKey: 取值属性键名, 默认 value |
|
|
|
showMask: 是否显示遮罩层, 默认 false |
|
|
|
--> |
|
|
|
<!-- 标签参数 |
|
|
|
touchdraggable: 是否启用拖动, 默认 false |
|
|
|
touchdroppable: 是否允许放置, 默认 false |
|
|
|
touchdraggroup: 拖拽分组, 默认 空(不限制), 只有同种分组的允许相互拖拽 |
|
|
|
--> |
|
|
|
<!-- 事件 |
|
|
|
touchdragstart: 开始拖动, 参数(element: 拖动的元素) |
|
|
|
touchdrop: 结束放置, 参数(src_element: 被拖动的元素, dst_element: 放置的元素, src_data: 被拖动元素的数据, dst_data: 放置元素的数据) |
|
|
|
touchdropstart: 开始放置, 参数(src_element: 被拖动的元素, dst_element: 放置的元素, src_data: 被拖动元素的数据, dst_data: 放置元素的数据) |
|
|
|
touchdragcancel: 中止拖动, 参数(element: 拖动的元素) |
|
|
|
touchdragmotion: 拖动中(坐标改变), 参数(element: 拖动的元素) |
|
|
|
--> |
|
|
|
<!-- 插槽 |
|
|
|
default: 内部元素 |
|
|
|
mask: 遮罩层内部元素 |
|
|
|
--> |
|
|
|
<!-- 事例 |
|
|
|
<DnD class="preview-cover van-ellipsis" @touchdragstart="drag" @touchdrop="drop" :value="file.index" touchdraggable touchdroppable show-mask="1" value-key="value"> |
|
|
|
<template #mask>遮罩层内部元素</template> |
|
|
|
<div>内部元素</div> |
|
|
|
</DnD> |
|
|
|
--> |
|
|
|
<div class="root" |
|
|
|
@touchstart.stop="Drag($event)" |
|
|
|
@touchend.stop="Drop($event)" |
|
|
|
@touchcandel.stop="Cancel($event)" |
|
|
|
@touchmove.stop="Motion($event)" |
|
|
|
> |
|
|
|
<slot name="default"></slot> |
|
|
|
<div class="mask" v-if="maskVisible" :style="{top: `${maskY}px`, left: `${maskX}px`, 'background-color': maskColor, 'border-color': maskColor}"> |
|
|
|
<slot name="mask"></slot> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
</template> |
|
|
|
|
|
|
|
<script> |
|
|
|
|
|
|
|
const STATE_NONE = 0; // 空闲状态 |
|
|
|
const STATE_START = 1; // 点击后等待完成长按状态 |
|
|
|
const STATE_DRAGGING = 2; // 长按状态完成, 进入拖拽状态 |
|
|
|
|
|
|
|
const HOLD_TIME_LIMIT = 200; // 从点击开始到可以允许拖拽间的最小时间间隔(模拟按住), 以区别于纯点击事件 |
|
|
|
const HOLD_POSITION_RANGE = 10/*px*/; // 从点击开始到可以允许拖拽间的最小时间间隔(模拟按住), 手指坐标距离开始点击坐标的范围限制, 以区别于手指纯滑动事件 |
|
|
|
|
|
|
|
const DRAGGABLE_NAME = 'touchdraggable'; // 允许拖动参数 |
|
|
|
const DROPPABLE_NAME = 'touchdroppable'; // 允许放置参数 |
|
|
|
const DRAGGROUP_NAME = 'touchdraggroup'; // 拖拽组名参数 |
|
|
|
|
|
|
|
const MASK_COLOR = 'rgba(0, 0, 0, 1)'; // 无放置目标时颜色 |
|
|
|
const MASK_DROP_COLOR = 'rgba(144,238,144, 1)'; // 有放置目标时颜色 |
|
|
|
const MASK_FORBID_COLOR = 'rgba(255,69,0, 1)'; // 禁止放置目标时颜色(放置到自身) |
|
|
|
|
|
|
|
export default { |
|
|
|
name: "DnD", |
|
|
|
props: ['valueKey', 'showMask', ], |
|
|
|
data() { |
|
|
|
return { |
|
|
|
data: null, // 拖动时存储的数据 |
|
|
|
state: STATE_NONE, // 当前拖拽状态 |
|
|
|
startX: -1, // 开始点击坐标(client) |
|
|
|
startY: -1, |
|
|
|
timerHandler: null, // 模拟按住的定时器 |
|
|
|
startTime: -1, // 开始点击时间 |
|
|
|
currentX: -1, // 当前触摸坐标(client) |
|
|
|
currentY: -1, |
|
|
|
element: null, // 被拖动的元素 |
|
|
|
dropElement: null, // 当前可放置的元素 |
|
|
|
|
|
|
|
startGlobalX: -1, // 开始点击坐标(page) |
|
|
|
startGlobalY: -1, |
|
|
|
maskX: 0, // 遮罩left = pageX - startGlobalX |
|
|
|
maskY: 0, // 遮罩top = pageY - startGlobalY |
|
|
|
maskColor: MASK_COLOR, // 遮罩提示颜色 |
|
|
|
}; |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
Drag(event) { |
|
|
|
if(!this.HasAttr(this.GetEventElement(event), DRAGGABLE_NAME)) |
|
|
|
return; |
|
|
|
if(this.state !== STATE_NONE) |
|
|
|
{ |
|
|
|
console.error('Drag', 'Only support single finger!'); |
|
|
|
return; |
|
|
|
} |
|
|
|
this.UpdateMask(event); |
|
|
|
this.element = this.GetEventElement(event); |
|
|
|
[this.startX, this.startY] = this.GetEventPosition(event); |
|
|
|
this.currentX = this.startX; |
|
|
|
this.currentY = this.startY; |
|
|
|
this.StartHold(); |
|
|
|
}, |
|
|
|
Drop(event) { |
|
|
|
if(!this.HasAttr(this.GetEventElement(event), DROPPABLE_NAME)) |
|
|
|
return; |
|
|
|
if(this.state !== STATE_DRAGGING) |
|
|
|
{ |
|
|
|
this.Reset(); |
|
|
|
return; |
|
|
|
} |
|
|
|
[this.currentX, this.currentY] = this.GetEventPosition(event); |
|
|
|
this.dropElement = this.GetCurrentPositionDropElement(event); |
|
|
|
this.UpdateMask(event, this.HasCurrentPositionDropElement(event)); |
|
|
|
if(this.dropElement) |
|
|
|
{ |
|
|
|
this.Log('touchdrop', this.data, this.GetValue(this.dropElement)); |
|
|
|
this.$emit('touchdrop', this.element, this.dropElement, this.data, this.GetValue(this.dropElement)); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
this.Log('touchcancel -> touchend'); |
|
|
|
this.$emit('touchdragcancel', this.element); |
|
|
|
} |
|
|
|
this.Reset(); |
|
|
|
this.state = STATE_NONE; |
|
|
|
}, |
|
|
|
Motion(event) { |
|
|
|
if(this.state === STATE_NONE) |
|
|
|
{ |
|
|
|
this.Reset(); |
|
|
|
return; |
|
|
|
} |
|
|
|
[this.currentX, this.currentY] = this.GetEventPosition(event); |
|
|
|
let dropElement = this.GetCurrentPositionDropElement(event); |
|
|
|
this.UpdateMask(event, this.HasCurrentPositionDropElement(event)); |
|
|
|
if(dropElement && this.dropElement !== dropElement) |
|
|
|
{ |
|
|
|
this.Log('touchdropstart', this.dropElement, dropElement); |
|
|
|
this.$emit('touchdropstart', this.element, dropElement, this.data, this.GetValue(dropElement)); |
|
|
|
} |
|
|
|
this.dropElement = dropElement; |
|
|
|
this.$emit('touchdragmotion', this.element); |
|
|
|
}, |
|
|
|
Cancel(event) { |
|
|
|
if(this.state === STATE_DRAGGING) |
|
|
|
{ |
|
|
|
this.Log('touchcancel -> touchcancel'); |
|
|
|
this.$emit('touchdragcancel', this.element); |
|
|
|
} |
|
|
|
this.Reset(); |
|
|
|
}, |
|
|
|
GetEventElement(event) { |
|
|
|
return event.target; |
|
|
|
}, |
|
|
|
GetEventPosition(event) { |
|
|
|
let x = event.changedTouches[0].clientX; |
|
|
|
let y = event.changedTouches[0].clientY; |
|
|
|
//this.Log('GetEventPosition', x, y); |
|
|
|
return [x, y]; |
|
|
|
}, |
|
|
|
GetEventViewportPosition(event) { |
|
|
|
let x = event.changedTouches[0].pageX; |
|
|
|
let y = event.changedTouches[0].pageY; |
|
|
|
//this.Log('GetEventViewportPosition', x, y); |
|
|
|
return [x, y]; |
|
|
|
}, |
|
|
|
GetElementByPosition(event, x, y) { |
|
|
|
let arr = document.elementsFromPoint(x, y); |
|
|
|
if(!arr) |
|
|
|
return null; |
|
|
|
let res = null; |
|
|
|
let srcEle = this.GetEventElement(event); |
|
|
|
for(let e of arr) |
|
|
|
{ |
|
|
|
if(this.IsDroppable(srcEle, e)) |
|
|
|
{ |
|
|
|
res = e; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
//this.Log('GetElementByPosition', res, x, y); |
|
|
|
return res; |
|
|
|
}, |
|
|
|
IsDroppable(srcElement, elememt) { |
|
|
|
let a = this.HasAttr(elememt, DROPPABLE_NAME); |
|
|
|
if(!a) |
|
|
|
return false; |
|
|
|
let e1 = srcElement.getAttribute(DRAGGROUP_NAME); |
|
|
|
let e2 = elememt.getAttribute(DRAGGROUP_NAME); |
|
|
|
return e1 == e2; |
|
|
|
}, |
|
|
|
Log(what, data) { |
|
|
|
//console.log(...arguments); |
|
|
|
//this.DEBUG(); |
|
|
|
}, |
|
|
|
StopHold() { |
|
|
|
if(null !== this.timerHandler) |
|
|
|
{ |
|
|
|
clearTimeout(this.timerHandler); |
|
|
|
this.timerHandler = null; |
|
|
|
} |
|
|
|
}, |
|
|
|
StartHold() { |
|
|
|
this.StopHold(); |
|
|
|
this.state = STATE_START; |
|
|
|
this.startTime = Date.now(); |
|
|
|
this.timerHandler = setTimeout(this.WaitHold, HOLD_TIME_LIMIT); |
|
|
|
}, |
|
|
|
WaitHold() { |
|
|
|
this.timerHandler = null; |
|
|
|
if(this.state !== STATE_START) |
|
|
|
{ |
|
|
|
this.Log('state cancel'); |
|
|
|
this.Reset(); |
|
|
|
return; |
|
|
|
} |
|
|
|
if(!this.CheckPosition()) |
|
|
|
{ |
|
|
|
this.Log('CheckPosition cancel'); |
|
|
|
this.Reset(); |
|
|
|
return; |
|
|
|
} |
|
|
|
this.state = STATE_DRAGGING; |
|
|
|
this.Log('touchdragstart', this.value); |
|
|
|
this.data = this.GetValue(this.element); |
|
|
|
this.$emit('touchdragstart', this.element); |
|
|
|
}, |
|
|
|
Reset() { |
|
|
|
this.StopHold(); |
|
|
|
this.state = STATE_NONE; |
|
|
|
this.startTime = -1; |
|
|
|
this.startX = this.startY = -1; |
|
|
|
this.currentX = this.currentY = -1; |
|
|
|
this.data = null; |
|
|
|
this.element = null; |
|
|
|
this.startGlobalX = this.startGlobalY = -1; |
|
|
|
this.maskX = this.maskY = 0; |
|
|
|
this.maskColor = MASK_COLOR; |
|
|
|
this.dropElement = null; |
|
|
|
}, |
|
|
|
CheckPosition() { |
|
|
|
if(this.startX < 0 || this.startY < 0) |
|
|
|
return false; |
|
|
|
if(this.currentX < 0 || this.currentY < 0) |
|
|
|
return false; |
|
|
|
let deltaX = this.currentX - this.startX; |
|
|
|
let deltaY = this.currentY - this.startY; |
|
|
|
return deltaX * deltaX + deltaY * deltaY <= HOLD_POSITION_RANGE * HOLD_POSITION_RANGE; |
|
|
|
}, |
|
|
|
DEBUG() { |
|
|
|
console.log( |
|
|
|
`state: ${this.state} |
|
|
|
start: [${this.startX}, ${this.startY}] |
|
|
|
startTime: ${this.startTime} |
|
|
|
timerHandler: ${this.timerHandler} |
|
|
|
data: ${this.data} |
|
|
|
current: [${this.currentX}, ${this.currentY}] |
|
|
|
`); |
|
|
|
}, |
|
|
|
HasAttr(element, name) { |
|
|
|
let a = element.getAttribute(name); |
|
|
|
return a !== null && a !== undefined; |
|
|
|
}, |
|
|
|
GetValue(element) { |
|
|
|
return element.getAttribute(this._valueKey) |
|
|
|
}, |
|
|
|
UpdateMask(event, element) { |
|
|
|
let [x, y] = this.GetEventViewportPosition(event); |
|
|
|
if(this.startGlobalX < 0) |
|
|
|
{ |
|
|
|
this.startGlobalX = x; |
|
|
|
} |
|
|
|
if(this.startGlobalY < 0) |
|
|
|
{ |
|
|
|
this.startGlobalY = y; |
|
|
|
} |
|
|
|
this.maskX = x - this.startGlobalX; |
|
|
|
this.maskY = y - this.startGlobalY; |
|
|
|
this.maskColor = element > 0 ? MASK_DROP_COLOR : (element < 0 ? MASK_COLOR : MASK_FORBID_COLOR); |
|
|
|
}, |
|
|
|
GetCurrentPositionDropElement(event) { |
|
|
|
let element = this.GetElementByPosition(event, this.currentX, this.currentY); |
|
|
|
if(element && element !== this.GetEventElement(event)) |
|
|
|
return element; |
|
|
|
return null; |
|
|
|
}, |
|
|
|
HasCurrentPositionDropElement(event) { |
|
|
|
let element = this.GetElementByPosition(event, this.currentX, this.currentY); |
|
|
|
if(!element) |
|
|
|
return -1; |
|
|
|
if(element === this.GetEventElement(event)) |
|
|
|
return 0; |
|
|
|
return 1; |
|
|
|
}, |
|
|
|
}, |
|
|
|
computed: { |
|
|
|
_valueKey() { |
|
|
|
return this.valueKey || 'value'; |
|
|
|
}, |
|
|
|
maskVisible() { |
|
|
|
return !!this.showMask && this.state === STATE_DRAGGING; |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
</script> |
|
|
|
|
|
|
|
<style scoped> |
|
|
|
.root { |
|
|
|
position: absolute; |
|
|
|
bottom: 0; |
|
|
|
top: 0; |
|
|
|
left: 0; |
|
|
|
right: 0; |
|
|
|
background: rgba(0, 0, 0, 0); |
|
|
|
overflow: visible; |
|
|
|
} |
|
|
|
.mask { |
|
|
|
position: absolute; |
|
|
|
top: 0; |
|
|
|
left: 0; |
|
|
|
width: 100%; |
|
|
|
height: 100%; |
|
|
|
background: rgba(0, 0, 0, 1); |
|
|
|
z-index: 99; |
|
|
|
opacity: 0.6; |
|
|
|
border-width: 4px; |
|
|
|
border-style: solid; |
|
|
|
} |
|
|
|
</style> |