|
- <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>
|