移动端
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. <template>
  2. <!-- 拖拽组件 DnD DragAndDrop -->
  3. <!-- 针对移动端draggable不生效的解决方案 父级元素定位必须为 position: relative; -->
  4. <!-- 属性
  5. valueKey: 取值属性键名, 默认 value
  6. showMask: 是否显示遮罩层, 默认 false
  7. -->
  8. <!-- 标签参数
  9. touchdraggable: 是否启用拖动, 默认 false
  10. touchdroppable: 是否允许放置, 默认 false
  11. touchdraggroup: 拖拽分组, 默认 空(不限制), 只有同种分组的允许相互拖拽
  12. -->
  13. <!-- 事件
  14. touchdragstart: 开始拖动, 参数(element: 拖动的元素)
  15. touchdrop: 结束放置, 参数(src_element: 被拖动的元素, dst_element: 放置的元素, src_data: 被拖动元素的数据, dst_data: 放置元素的数据)
  16. touchdropstart: 开始放置, 参数(src_element: 被拖动的元素, dst_element: 放置的元素, src_data: 被拖动元素的数据, dst_data: 放置元素的数据)
  17. touchdragcancel: 中止拖动, 参数(element: 拖动的元素)
  18. touchdragmotion: 拖动中(坐标改变), 参数(element: 拖动的元素)
  19. -->
  20. <!-- 插槽
  21. default: 内部元素
  22. mask: 遮罩层内部元素
  23. -->
  24. <!-- 事例
  25. <DnD class="preview-cover van-ellipsis" @touchdragstart="drag" @touchdrop="drop" :value="file.index" touchdraggable touchdroppable show-mask="1" value-key="value">
  26. <template #mask>遮罩层内部元素</template>
  27. <div>内部元素</div>
  28. </DnD>
  29. -->
  30. <div class="root"
  31. @touchstart.stop="Drag($event)"
  32. @touchend.stop="Drop($event)"
  33. @touchcandel.stop="Cancel($event)"
  34. @touchmove.stop="Motion($event)"
  35. >
  36. <slot name="default"></slot>
  37. <div class="mask" v-if="maskVisible" :style="{top: `${maskY}px`, left: `${maskX}px`, 'background-color': maskColor, 'border-color': maskColor}">
  38. <slot name="mask"></slot>
  39. </div>
  40. </div>
  41. </template>
  42. <script>
  43. const STATE_NONE = 0; // 空闲状态
  44. const STATE_START = 1; // 点击后等待完成长按状态
  45. const STATE_DRAGGING = 2; // 长按状态完成, 进入拖拽状态
  46. const HOLD_TIME_LIMIT = 200; // 从点击开始到可以允许拖拽间的最小时间间隔(模拟按住), 以区别于纯点击事件
  47. const HOLD_POSITION_RANGE = 10/*px*/; // 从点击开始到可以允许拖拽间的最小时间间隔(模拟按住), 手指坐标距离开始点击坐标的范围限制, 以区别于手指纯滑动事件
  48. const DRAGGABLE_NAME = 'touchdraggable'; // 允许拖动参数
  49. const DROPPABLE_NAME = 'touchdroppable'; // 允许放置参数
  50. const DRAGGROUP_NAME = 'touchdraggroup'; // 拖拽组名参数
  51. const MASK_COLOR = 'rgba(0, 0, 0, 1)'; // 无放置目标时颜色
  52. const MASK_DROP_COLOR = 'rgba(144,238,144, 1)'; // 有放置目标时颜色
  53. const MASK_FORBID_COLOR = 'rgba(255,69,0, 1)'; // 禁止放置目标时颜色(放置到自身)
  54. export default {
  55. name: "DnD",
  56. props: ['valueKey', 'showMask', ],
  57. data() {
  58. return {
  59. data: null, // 拖动时存储的数据
  60. state: STATE_NONE, // 当前拖拽状态
  61. startX: -1, // 开始点击坐标(client)
  62. startY: -1,
  63. timerHandler: null, // 模拟按住的定时器
  64. startTime: -1, // 开始点击时间
  65. currentX: -1, // 当前触摸坐标(client)
  66. currentY: -1,
  67. element: null, // 被拖动的元素
  68. dropElement: null, // 当前可放置的元素
  69. startGlobalX: -1, // 开始点击坐标(page)
  70. startGlobalY: -1,
  71. maskX: 0, // 遮罩left = pageX - startGlobalX
  72. maskY: 0, // 遮罩top = pageY - startGlobalY
  73. maskColor: MASK_COLOR, // 遮罩提示颜色
  74. };
  75. },
  76. methods: {
  77. Drag(event) {
  78. if(!this.HasAttr(this.GetEventElement(event), DRAGGABLE_NAME))
  79. return;
  80. if(this.state !== STATE_NONE)
  81. {
  82. console.error('Drag', 'Only support single finger!');
  83. return;
  84. }
  85. this.UpdateMask(event);
  86. this.element = this.GetEventElement(event);
  87. [this.startX, this.startY] = this.GetEventPosition(event);
  88. this.currentX = this.startX;
  89. this.currentY = this.startY;
  90. this.StartHold();
  91. },
  92. Drop(event) {
  93. if(!this.HasAttr(this.GetEventElement(event), DROPPABLE_NAME))
  94. return;
  95. if(this.state !== STATE_DRAGGING)
  96. {
  97. this.Reset();
  98. return;
  99. }
  100. [this.currentX, this.currentY] = this.GetEventPosition(event);
  101. this.dropElement = this.GetCurrentPositionDropElement(event);
  102. this.UpdateMask(event, this.HasCurrentPositionDropElement(event));
  103. if(this.dropElement)
  104. {
  105. this.Log('touchdrop', this.data, this.GetValue(this.dropElement));
  106. this.$emit('touchdrop', this.element, this.dropElement, this.data, this.GetValue(this.dropElement));
  107. }
  108. else
  109. {
  110. this.Log('touchcancel -> touchend');
  111. this.$emit('touchdragcancel', this.element);
  112. }
  113. this.Reset();
  114. this.state = STATE_NONE;
  115. },
  116. Motion(event) {
  117. if(this.state === STATE_NONE)
  118. {
  119. this.Reset();
  120. return;
  121. }
  122. [this.currentX, this.currentY] = this.GetEventPosition(event);
  123. let dropElement = this.GetCurrentPositionDropElement(event);
  124. this.UpdateMask(event, this.HasCurrentPositionDropElement(event));
  125. if(dropElement && this.dropElement !== dropElement)
  126. {
  127. this.Log('touchdropstart', this.dropElement, dropElement);
  128. this.$emit('touchdropstart', this.element, dropElement, this.data, this.GetValue(dropElement));
  129. }
  130. this.dropElement = dropElement;
  131. this.$emit('touchdragmotion', this.element);
  132. },
  133. Cancel(event) {
  134. if(this.state === STATE_DRAGGING)
  135. {
  136. this.Log('touchcancel -> touchcancel');
  137. this.$emit('touchdragcancel', this.element);
  138. }
  139. this.Reset();
  140. },
  141. GetEventElement(event) {
  142. return event.target;
  143. },
  144. GetEventPosition(event) {
  145. let x = event.changedTouches[0].clientX;
  146. let y = event.changedTouches[0].clientY;
  147. //this.Log('GetEventPosition', x, y);
  148. return [x, y];
  149. },
  150. GetEventViewportPosition(event) {
  151. let x = event.changedTouches[0].pageX;
  152. let y = event.changedTouches[0].pageY;
  153. //this.Log('GetEventViewportPosition', x, y);
  154. return [x, y];
  155. },
  156. GetElementByPosition(event, x, y) {
  157. let arr = document.elementsFromPoint(x, y);
  158. if(!arr)
  159. return null;
  160. let res = null;
  161. let srcEle = this.GetEventElement(event);
  162. for(let e of arr)
  163. {
  164. if(this.IsDroppable(srcEle, e))
  165. {
  166. res = e;
  167. break;
  168. }
  169. }
  170. //this.Log('GetElementByPosition', res, x, y);
  171. return res;
  172. },
  173. IsDroppable(srcElement, elememt) {
  174. let a = this.HasAttr(elememt, DROPPABLE_NAME);
  175. if(!a)
  176. return false;
  177. let e1 = srcElement.getAttribute(DRAGGROUP_NAME);
  178. let e2 = elememt.getAttribute(DRAGGROUP_NAME);
  179. return e1 == e2;
  180. },
  181. Log(what, data) {
  182. //console.log(...arguments);
  183. //this.DEBUG();
  184. },
  185. StopHold() {
  186. if(null !== this.timerHandler)
  187. {
  188. clearTimeout(this.timerHandler);
  189. this.timerHandler = null;
  190. }
  191. },
  192. StartHold() {
  193. this.StopHold();
  194. this.state = STATE_START;
  195. this.startTime = Date.now();
  196. this.timerHandler = setTimeout(this.WaitHold, HOLD_TIME_LIMIT);
  197. },
  198. WaitHold() {
  199. this.timerHandler = null;
  200. if(this.state !== STATE_START)
  201. {
  202. this.Log('state cancel');
  203. this.Reset();
  204. return;
  205. }
  206. if(!this.CheckPosition())
  207. {
  208. this.Log('CheckPosition cancel');
  209. this.Reset();
  210. return;
  211. }
  212. this.state = STATE_DRAGGING;
  213. this.Log('touchdragstart', this.value);
  214. this.data = this.GetValue(this.element);
  215. this.$emit('touchdragstart', this.element);
  216. },
  217. Reset() {
  218. this.StopHold();
  219. this.state = STATE_NONE;
  220. this.startTime = -1;
  221. this.startX = this.startY = -1;
  222. this.currentX = this.currentY = -1;
  223. this.data = null;
  224. this.element = null;
  225. this.startGlobalX = this.startGlobalY = -1;
  226. this.maskX = this.maskY = 0;
  227. this.maskColor = MASK_COLOR;
  228. this.dropElement = null;
  229. },
  230. CheckPosition() {
  231. if(this.startX < 0 || this.startY < 0)
  232. return false;
  233. if(this.currentX < 0 || this.currentY < 0)
  234. return false;
  235. let deltaX = this.currentX - this.startX;
  236. let deltaY = this.currentY - this.startY;
  237. return deltaX * deltaX + deltaY * deltaY <= HOLD_POSITION_RANGE * HOLD_POSITION_RANGE;
  238. },
  239. DEBUG() {
  240. console.log(
  241. `state: ${this.state}
  242. start: [${this.startX}, ${this.startY}]
  243. startTime: ${this.startTime}
  244. timerHandler: ${this.timerHandler}
  245. data: ${this.data}
  246. current: [${this.currentX}, ${this.currentY}]
  247. `);
  248. },
  249. HasAttr(element, name) {
  250. let a = element.getAttribute(name);
  251. return a !== null && a !== undefined;
  252. },
  253. GetValue(element) {
  254. return element.getAttribute(this._valueKey)
  255. },
  256. UpdateMask(event, element) {
  257. let [x, y] = this.GetEventViewportPosition(event);
  258. if(this.startGlobalX < 0)
  259. {
  260. this.startGlobalX = x;
  261. }
  262. if(this.startGlobalY < 0)
  263. {
  264. this.startGlobalY = y;
  265. }
  266. this.maskX = x - this.startGlobalX;
  267. this.maskY = y - this.startGlobalY;
  268. this.maskColor = element > 0 ? MASK_DROP_COLOR : (element < 0 ? MASK_COLOR : MASK_FORBID_COLOR);
  269. },
  270. GetCurrentPositionDropElement(event) {
  271. let element = this.GetElementByPosition(event, this.currentX, this.currentY);
  272. if(element && element !== this.GetEventElement(event))
  273. return element;
  274. return null;
  275. },
  276. HasCurrentPositionDropElement(event) {
  277. let element = this.GetElementByPosition(event, this.currentX, this.currentY);
  278. if(!element)
  279. return -1;
  280. if(element === this.GetEventElement(event))
  281. return 0;
  282. return 1;
  283. },
  284. },
  285. computed: {
  286. _valueKey() {
  287. return this.valueKey || 'value';
  288. },
  289. maskVisible() {
  290. return !!this.showMask && this.state === STATE_DRAGGING;
  291. },
  292. },
  293. }
  294. </script>
  295. <style scoped>
  296. .root {
  297. position: absolute;
  298. bottom: 0;
  299. top: 0;
  300. left: 0;
  301. right: 0;
  302. background: rgba(0, 0, 0, 0);
  303. overflow: visible;
  304. }
  305. .mask {
  306. position: absolute;
  307. top: 0;
  308. left: 0;
  309. width: 100%;
  310. height: 100%;
  311. background: rgba(0, 0, 0, 1);
  312. z-index: 99;
  313. opacity: 0.6;
  314. border-width: 4px;
  315. border-style: solid;
  316. }
  317. </style>