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