Draggable.vue 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. <script setup lang="ts">
  2. import { PropType, Slot as SlotType, watch, onMounted, ref } from "vue";
  3. const props = defineProps({
  4. itemKey: { type: String, default: "" },
  5. list: { type: Array as PropType<any[]>, default: () => [] },
  6. attributes: { type: Object, default: () => ({}) },
  7. tag: { type: String, default: "div" },
  8. class: { type: String, default: "" },
  9. group: { type: String, default: "" },
  10. disabled: { type: [Boolean, Function], default: false }
  11. });
  12. const listUuid = ref(
  13. "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, symbol => {
  14. let array;
  15. if (symbol === "y") {
  16. array = ["8", "9", "a", "b"];
  17. return array[Math.floor(Math.random() * array.length)];
  18. }
  19. array = new Uint8Array(1);
  20. window.crypto.getRandomValues(array);
  21. return (array[0] % 16).toString(16);
  22. })
  23. );
  24. const mounted = ref(false);
  25. const data = ref([] as any[]);
  26. watch(
  27. () => props.list,
  28. list => {
  29. data.value = list;
  30. }
  31. );
  32. onMounted(() => {
  33. data.value = props.list;
  34. mounted.value = true;
  35. });
  36. const emit = defineEmits(["update:list", "start", "end", "update"]);
  37. const itemOnMove = (index: number) => {
  38. // Deletes the remove function for the dragging element
  39. if (window.draggingItem && window.draggingItem.itemOnMove)
  40. delete window.draggingItem.itemOnMove;
  41. // Remove the item from the current list and return it
  42. const listItem = data.value.splice(index, 1)[0];
  43. emit("update:list", data.value);
  44. return listItem;
  45. };
  46. // When an element starts being dragged
  47. const onDragStart = (itemIndex: number, event: DragEvent) => {
  48. const { draggable } = event.target as HTMLElement;
  49. if (props.disabled === true || !draggable || !event.dataTransfer) {
  50. event.preventDefault();
  51. return;
  52. }
  53. // Set the effect of moving an element, which by default is clone. Not being used right now
  54. event.dataTransfer.dropEffect = "move";
  55. // Sets the dragging element index, list uuid and adds a remove function for when this item is moved to a different list
  56. window.draggingItem = {
  57. itemIndex,
  58. itemListUuid: listUuid.value,
  59. itemGroup: props.group,
  60. itemOnMove,
  61. initialItemIndex: itemIndex,
  62. initialItemListUuid: listUuid.value
  63. };
  64. // Emits the start event to the parent component, indicating that dragging has started
  65. emit("start");
  66. };
  67. // When a dragging element hovers over another draggable element, this gets triggered, usually many times in a second
  68. const onDragOver = (itemIndex: number, event: DragEvent) => {
  69. const getDraggableElement = (element: any): any =>
  70. element.classList.contains("draggable-item")
  71. ? element
  72. : getDraggableElement(element.parentElement);
  73. const draggableElement = getDraggableElement(event.target);
  74. const { draggable } = draggableElement;
  75. if (props.disabled === true || !draggable || !window.draggingItem) return;
  76. // The index and list uuid of the item that is being dragged, stored in window since it can come from another list as well
  77. const fromIndex = window.draggingItem.itemIndex;
  78. const fromList = window.draggingItem.itemListUuid;
  79. // The new index and list uuid of the item that is being dragged
  80. const toIndex = itemIndex;
  81. const toList = listUuid.value;
  82. // Don't continue if fromIndex is invalid
  83. if (fromIndex === -1 || toIndex === -1) return;
  84. // If the item hasn't changed position in the same list, don't continue
  85. if (fromIndex === toIndex && fromList === toList) return;
  86. // If the dragging item isn't from the same group, don't continue
  87. if (
  88. fromList !== toList &&
  89. (props.group === "" || window.draggingItem.itemGroup !== props.group)
  90. )
  91. return;
  92. // Update the index and list uuid of the dragged item
  93. window.draggingItem.itemIndex = toIndex;
  94. window.draggingItem.itemListUuid = listUuid.value;
  95. // If the item comes from another list
  96. if (toList !== fromList && window.draggingItem.itemOnMove) {
  97. // Call the remove function from the dragging element, which removes the item from the previous list and returns it
  98. const item = window.draggingItem.itemOnMove(fromIndex);
  99. // Define a new remove function for the dragging element
  100. window.draggingItem.itemOnMove = itemOnMove;
  101. window.draggingItem.itemGroup = props.group;
  102. // Add the item to the list at the new index
  103. data.value.splice(toIndex, 0, item);
  104. emit("update:list", data.value);
  105. }
  106. // If the item is being reordered in the same list
  107. else {
  108. // Remove the item from the old position, and add the item to the new position
  109. data.value.splice(toIndex, 0, data.value.splice(fromIndex, 1)[0]);
  110. emit("update:list", data.value);
  111. }
  112. };
  113. // Gets called when the element that is being dragged is released
  114. const onDragEnd = () => {
  115. // Emits the end event to parent component, indicating that dragging has ended
  116. emit("end");
  117. };
  118. // Gets called when an element is dropped on another element
  119. const onDrop = () => {
  120. // Emits the update event to parent component, indicating that the order is now done and ordering/moving is done
  121. if (!window.draggingItem) return;
  122. const { itemIndex, itemListUuid, initialItemIndex, initialItemListUuid } =
  123. window.draggingItem;
  124. if (itemListUuid === initialItemListUuid)
  125. emit("update", {
  126. moved: {
  127. oldIndex: initialItemIndex,
  128. newIndex: itemIndex,
  129. updatedList: data.value
  130. }
  131. });
  132. else emit("update", {});
  133. delete window.draggingItem;
  134. };
  135. // Function that gets called for each item and returns attributes
  136. const convertAttributes = (item: any) =>
  137. Object.fromEntries(
  138. Object.entries(props.attributes).map(([key, value]) => [
  139. key,
  140. typeof value === "function" ? value(item) : value
  141. ])
  142. );
  143. const hasSlotContent = (slot: SlotType | undefined, slotProps = {}) => {
  144. if (!slot) return false;
  145. return slot(slotProps).some(vnode => {
  146. if (
  147. vnode.type === Comment ||
  148. vnode.type.toString() === "Symbol(Comment)"
  149. )
  150. return false;
  151. if (Array.isArray(vnode.children) && !vnode.children.length)
  152. return false;
  153. return (
  154. vnode.type !== Text ||
  155. vnode.type.toString() !== "Symbol(Text)" ||
  156. (typeof vnode.children === "string" && vnode.children.trim() !== "")
  157. );
  158. });
  159. };
  160. </script>
  161. <template>
  162. <template v-for="(item, itemIndex) in data" :key="item[itemKey]">
  163. <component
  164. v-if="$slots.item && hasSlotContent($slots.item, { element: item })"
  165. :is="tag"
  166. :draggable="
  167. typeof disabled === 'function' ? !disabled(item) : !disabled
  168. "
  169. @dragstart="onDragStart(itemIndex, $event)"
  170. @dragenter.prevent
  171. @dragover.prevent="onDragOver(itemIndex, $event)"
  172. @dragend="onDragEnd()"
  173. @drop.prevent="onDrop()"
  174. :data-index="itemIndex"
  175. :data-list="listUuid"
  176. class="draggable-item"
  177. v-bind="convertAttributes(item)"
  178. >
  179. <slot name="item" :element="item" :index="itemIndex"></slot>
  180. </component>
  181. </template>
  182. </template>
  183. <style scoped>
  184. .draggable-item[draggable="true"] {
  185. cursor: move;
  186. }
  187. .draggable-item:not(:last-of-type) {
  188. margin-bottom: 10px;
  189. }
  190. </style>