feat: drag en drop werkt op mobiel
This commit is contained in:
		
							parent
							
								
									45563b68ea
								
							
						
					
					
						commit
						862e72ef4a
					
				
					 4 changed files with 489 additions and 277 deletions
				
			
		|  | @ -21,6 +21,7 @@ | ||||||
|         "@tanstack/vue-query": "^5.69.0", |         "@tanstack/vue-query": "^5.69.0", | ||||||
|         "@vueuse/core": "^13.1.0", |         "@vueuse/core": "^13.1.0", | ||||||
|         "axios": "^1.8.2", |         "axios": "^1.8.2", | ||||||
|  |         "interactjs": "^1.10.27", | ||||||
|         "oidc-client-ts": "^3.1.0", |         "oidc-client-ts": "^3.1.0", | ||||||
|         "rollup": "^4.40.0", |         "rollup": "^4.40.0", | ||||||
|         "uuid": "^11.1.0", |         "uuid": "^11.1.0", | ||||||
|  |  | ||||||
|  | @ -1,38 +1,38 @@ | ||||||
| <script setup lang="ts"> | <script setup lang="ts"> | ||||||
|     import { computed, ref, watch } from "vue"; | import {computed, ref, watch} from "vue"; | ||||||
|     import { useI18n } from "vue-i18n"; | import {useI18n} from "vue-i18n"; | ||||||
|     import { useClassStudentsQuery } from "@/queries/classes"; | import {useClassStudentsQuery} from "@/queries/classes"; | ||||||
| 
 | 
 | ||||||
|     const props = defineProps<{ | const props = defineProps<{ | ||||||
|     classId: string | undefined; |     classId: string | undefined; | ||||||
|     groups: object[]; |     groups: object[]; | ||||||
|     }>(); | }>(); | ||||||
|     const emit = defineEmits(["close", "groupsUpdated", "done"]); | const emit = defineEmits(["close", "groupsUpdated", "done"]); | ||||||
|     const { t } = useI18n(); | const {t} = useI18n(); | ||||||
| 
 | 
 | ||||||
|     interface StudentItem { | interface StudentItem { | ||||||
|     username: string; |     username: string; | ||||||
|     fullName: string; |     fullName: string; | ||||||
|     } | } | ||||||
| 
 | 
 | ||||||
|     const { data: studentsData } = useClassStudentsQuery(() => props.classId, true); | const {data: studentsData} = useClassStudentsQuery(() => props.classId, true); | ||||||
| 
 | 
 | ||||||
|     // Dialog states | // Dialog states for group editing | ||||||
|     const activeDialog = ref<"random" | "dragdrop" | null>(null); | const activeDialog = ref<"random" | "dragdrop" | null>(null); | ||||||
| 
 | 
 | ||||||
|     // Drag state | // Drag state for the drag and drop | ||||||
|     const draggedItem = ref<{groupIndex: number, studentIndex: number} | null>(null); | const draggedItem = ref<{ groupIndex: number, studentIndex: number } | null>(null); | ||||||
| 
 | 
 | ||||||
|     const currentGroups = ref<StudentItem[][]>([]); | const currentGroups = ref<StudentItem[][]>([]); | ||||||
|     const unassignedStudents = ref<StudentItem[]>([]); | const unassignedStudents = ref<StudentItem[]>([]); | ||||||
|     const allStudents = ref<StudentItem[]>([]); | const allStudents = ref<StudentItem[]>([]); | ||||||
| 
 | 
 | ||||||
|     // Random groups state | // Random groups state | ||||||
|     const groupSize = ref(1); | const groupSize = ref(1); | ||||||
|     const randomGroupsPreview = ref<StudentItem[][]>([]); | const randomGroupsPreview = ref<StudentItem[][]>([]); | ||||||
| 
 | 
 | ||||||
|     // Initialize data | // Initialize data | ||||||
|     watch( | watch( | ||||||
|     () => [studentsData.value, props.groups], |     () => [studentsData.value, props.groups], | ||||||
|     ([studentsVal, existingGroups]) => { |     ([studentsVal, existingGroups]) => { | ||||||
|         if (!studentsVal) return; |         if (!studentsVal) return; | ||||||
|  | @ -56,20 +56,17 @@ | ||||||
|             ); |             ); | ||||||
|             unassignedStudents.value = allStudents.value.filter((s) => !assignedUsernames.has(s.username)); |             unassignedStudents.value = allStudents.value.filter((s) => !assignedUsernames.has(s.username)); | ||||||
|         } else { |         } else { | ||||||
|                 // Default to all students unassigned |  | ||||||
|             currentGroups.value = []; |             currentGroups.value = []; | ||||||
|             unassignedStudents.value = [...allStudents.value]; |             unassignedStudents.value = [...allStudents.value]; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|             // Initialize random preview with current groups |  | ||||||
|         randomGroupsPreview.value = [...currentGroups.value]; |         randomGroupsPreview.value = [...currentGroups.value]; | ||||||
|     }, |     }, | ||||||
|         { immediate: true }, |     {immediate: true}, | ||||||
|     ); | ); | ||||||
| 
 | 
 | ||||||
| 
 | /** Random groups functions */ | ||||||
|     // Random groups functions | function generateRandomGroups(): void { | ||||||
|     function generateRandomGroups() { |  | ||||||
|     if (groupSize.value < 1) return; |     if (groupSize.value < 1) return; | ||||||
| 
 | 
 | ||||||
|     // Shuffle students |     // Shuffle students | ||||||
|  | @ -90,9 +87,9 @@ | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     randomGroupsPreview.value = newGroups; |     randomGroupsPreview.value = newGroups; | ||||||
|     } | } | ||||||
| 
 | 
 | ||||||
|     function saveRandomGroups() { | function saveRandomGroups(): void { | ||||||
|     if (randomGroupsPreview.value.length === 0) { |     if (randomGroupsPreview.value.length === 0) { | ||||||
|         alert(t("please-generate-groups-first")); |         alert(t("please-generate-groups-first")); | ||||||
|         return; |         return; | ||||||
|  | @ -105,55 +102,223 @@ | ||||||
|     activeDialog.value = null; |     activeDialog.value = null; | ||||||
|     emit("done"); |     emit("done"); | ||||||
|     emit("close"); |     emit("close"); | ||||||
|     } | } | ||||||
| 
 | 
 | ||||||
|     // Drag and drop functions | function addNewGroup() { | ||||||
|     function addNewGroup() { |  | ||||||
|     currentGroups.value.push([]); |     currentGroups.value.push([]); | ||||||
|     } | } | ||||||
| 
 | 
 | ||||||
|     function removeGroup(index: number) { | function removeGroup(index: number) { | ||||||
|     // Move students back to unassigned |     // Move students back to unassigned | ||||||
|     unassignedStudents.value.push(...currentGroups.value[index]); |     unassignedStudents.value.push(...currentGroups.value[index]); | ||||||
|     currentGroups.value.splice(index, 1); |     currentGroups.value.splice(index, 1); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Drag and drop functions */ | ||||||
|  | 
 | ||||||
|  | // Touch state interface | ||||||
|  | interface TouchState { | ||||||
|  |     isDragging: boolean; | ||||||
|  |     startX: number; | ||||||
|  |     startY: number; | ||||||
|  |     currentGroupIndex: number; | ||||||
|  |     currentStudentIndex: number; | ||||||
|  |     element: HTMLElement | null; | ||||||
|  |     clone: HTMLElement | null; | ||||||
|  |     originalRect: DOMRect | null; | ||||||
|  |     hasMoved: boolean; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const touchState = ref<TouchState>({ | ||||||
|  |     isDragging: false, | ||||||
|  |     startX: 0, | ||||||
|  |     startY: 0, | ||||||
|  |     currentGroupIndex: -1, | ||||||
|  |     currentStudentIndex: -1, | ||||||
|  |     element: null, | ||||||
|  |     clone: null, | ||||||
|  |     originalRect: null, | ||||||
|  |     hasMoved: false | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | function handleTouchStart(event: TouchEvent, groupIndex: number, studentIndex: number): void { | ||||||
|  |     if (event.touches.length > 1) return; | ||||||
|  | 
 | ||||||
|  |     const touch = event.touches[0]; | ||||||
|  |     const target = event.target as HTMLElement; | ||||||
|  |     // Target the chip directly instead of the draggable container | ||||||
|  |     const chip = target.closest('.v-chip') as HTMLElement; | ||||||
|  | 
 | ||||||
|  |     if (!chip) return; | ||||||
|  | 
 | ||||||
|  |     // Get the chip's position relative to the viewport | ||||||
|  |     const rect = chip.getBoundingClientRect(); | ||||||
|  | 
 | ||||||
|  |     touchState.value = { | ||||||
|  |         isDragging: true, | ||||||
|  |         startX: touch.clientX, | ||||||
|  |         startY: touch.clientY, | ||||||
|  |         currentGroupIndex: groupIndex, | ||||||
|  |         currentStudentIndex: studentIndex, | ||||||
|  |         element: chip, | ||||||
|  |         clone: null, | ||||||
|  |         originalRect: rect, | ||||||
|  |         hasMoved: false | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // Clone only the chip | ||||||
|  |     const clone = chip.cloneNode(true) as HTMLElement; | ||||||
|  |     clone.classList.add('drag-clone'); | ||||||
|  |     clone.style.position = 'fixed'; | ||||||
|  |     clone.style.zIndex = '10000'; | ||||||
|  |     clone.style.opacity = '0.9'; | ||||||
|  |     clone.style.pointerEvents = 'none'; | ||||||
|  |     clone.style.width = `${rect.width}px`; | ||||||
|  |     clone.style.height = `${rect.height}px`; | ||||||
|  |     clone.style.left = `${rect.left}px`; | ||||||
|  |     clone.style.top = `${rect.top}px`; | ||||||
|  |     clone.style.transform = 'scale(1.05)'; | ||||||
|  |     clone.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)'; | ||||||
|  |     clone.style.transition = 'transform 0.1s'; | ||||||
|  | 
 | ||||||
|  |     // Ensure the clone has the same chip styling | ||||||
|  |     clone.style.backgroundColor = getComputedStyle(chip).backgroundColor; | ||||||
|  |     clone.style.color = getComputedStyle(chip).color; | ||||||
|  |     clone.style.borderRadius = getComputedStyle(chip).borderRadius; | ||||||
|  |     clone.style.padding = getComputedStyle(chip).padding; | ||||||
|  |     clone.style.margin = '0'; // Remove any margin | ||||||
|  | 
 | ||||||
|  |     document.body.appendChild(clone); | ||||||
|  |     touchState.value.clone = clone; | ||||||
|  |     chip.style.visibility = 'hidden'; | ||||||
|  | 
 | ||||||
|  |     event.preventDefault(); | ||||||
|  |     event.stopPropagation(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function handleTouchMove(event: TouchEvent): void { | ||||||
|  |     if (!touchState.value.isDragging || !touchState.value.clone || event.touches.length > 1) return; | ||||||
|  | 
 | ||||||
|  |     const touch = event.touches[0]; | ||||||
|  |     const clone = touchState.value.clone; | ||||||
|  | 
 | ||||||
|  |     const dx = Math.abs(touch.clientX - touchState.value.startX); | ||||||
|  |     const dy = Math.abs(touch.clientY - touchState.value.startY); | ||||||
|  | 
 | ||||||
|  |     if (dx > 5 || dy > 5) { | ||||||
|  |         touchState.value.hasMoved = true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Native Drag & Drop Handlers |     clone.style.left = `${touch.clientX - clone.offsetWidth / 2}px`; | ||||||
|     function handleDragStart(groupIndex: number, studentIndex: number) { |     clone.style.top = `${touch.clientY - clone.offsetHeight / 2}px`; | ||||||
|         draggedItem.value = { groupIndex, studentIndex }; | 
 | ||||||
|  |     document.querySelectorAll('.group-box').forEach(el => { | ||||||
|  |         el.classList.remove('highlight'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const elements = document.elementsFromPoint(touch.clientX, touch.clientY); | ||||||
|  |     const dropTarget = elements.find(el => el.classList.contains('group-box')); | ||||||
|  | 
 | ||||||
|  |     if (dropTarget) { | ||||||
|  |         dropTarget.classList.add('highlight'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function handleDragOver(e: DragEvent, _: number) { |     event.preventDefault(); | ||||||
|  |     event.stopPropagation(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function handleTouchEnd(event: TouchEvent): void { | ||||||
|  |     if (!touchState.value.isDragging) return; | ||||||
|  | 
 | ||||||
|  |     const { | ||||||
|  |         currentGroupIndex, | ||||||
|  |         currentStudentIndex, | ||||||
|  |         clone, | ||||||
|  |         element, | ||||||
|  |         hasMoved | ||||||
|  |     } = touchState.value; | ||||||
|  | 
 | ||||||
|  |     document.querySelectorAll('.group-box').forEach(el => { | ||||||
|  |         el.classList.remove('highlight'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (clone?.parentNode) { | ||||||
|  |         clone.parentNode.removeChild(clone); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (element) { | ||||||
|  |         element.style.visibility = 'visible'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (hasMoved && event.changedTouches.length > 0) { | ||||||
|  |         const touch = event.changedTouches[0]; | ||||||
|  |         const elements = document.elementsFromPoint(touch.clientX, touch.clientY); | ||||||
|  |         const dropTarget = elements.find(el => el.classList.contains('group-box')); | ||||||
|  | 
 | ||||||
|  |         if (dropTarget) { | ||||||
|  |             const groupBoxes = document.querySelectorAll('.group-box'); | ||||||
|  |             const targetGroupIndex = Array.from(groupBoxes).indexOf(dropTarget); | ||||||
|  | 
 | ||||||
|  |             if (targetGroupIndex !== currentGroupIndex) { | ||||||
|  |                 const sourceArray = currentGroupIndex === -1 | ||||||
|  |                     ? unassignedStudents.value | ||||||
|  |                     : currentGroups.value[currentGroupIndex]; | ||||||
|  |                 const targetArray = targetGroupIndex === -1 | ||||||
|  |                     ? unassignedStudents.value | ||||||
|  |                     : currentGroups.value[targetGroupIndex]; | ||||||
|  | 
 | ||||||
|  |                 if (sourceArray && targetArray) { | ||||||
|  |                     const [movedStudent] = sourceArray.splice(currentStudentIndex, 1); | ||||||
|  |                     targetArray.push(movedStudent); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     touchState.value = { | ||||||
|  |         isDragging: false, | ||||||
|  |         startX: 0, | ||||||
|  |         startY: 0, | ||||||
|  |         currentGroupIndex: -1, | ||||||
|  |         currentStudentIndex: -1, | ||||||
|  |         element: null, | ||||||
|  |         clone: null, | ||||||
|  |         originalRect: null, | ||||||
|  |         hasMoved: false | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     event.preventDefault(); | ||||||
|  |     event.stopPropagation(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function handleDragStart(event: DragEvent, groupIndex: number, studentIndex: number): void { | ||||||
|  |     draggedItem.value = {groupIndex, studentIndex}; | ||||||
|  |     if (event.dataTransfer) { | ||||||
|  |         event.dataTransfer.effectAllowed = 'move'; | ||||||
|  |         event.dataTransfer.setData('text/plain', ''); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function handleDragOver(e: DragEvent, _: number): void { | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|         e.dataTransfer!.dropEffect = "move"; |     if (e.dataTransfer) { | ||||||
|  |         e.dataTransfer.dropEffect = "move"; | ||||||
|     } |     } | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     function handleDrop(e: DragEvent, targetGroupIndex: number, targetStudentIndex?: number) { | function handleDrop(e: DragEvent, targetGroupIndex: number, targetStudentIndex?: number): void { | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|     if (!draggedItem.value) return; |     if (!draggedItem.value) return; | ||||||
| 
 | 
 | ||||||
|         const { groupIndex: sourceGroupIndex, studentIndex: sourceStudentIndex } = draggedItem.value; |     const {groupIndex: sourceGroupIndex, studentIndex: sourceStudentIndex} = draggedItem.value; | ||||||
|         const isSameGroup = sourceGroupIndex === targetGroupIndex; |     const sourceArray = sourceGroupIndex === -1 | ||||||
|  |         ? unassignedStudents.value | ||||||
|  |         : currentGroups.value[sourceGroupIndex]; | ||||||
|  |     const targetArray = targetGroupIndex === -1 | ||||||
|  |         ? unassignedStudents.value | ||||||
|  |         : currentGroups.value[targetGroupIndex]; | ||||||
| 
 | 
 | ||||||
|         let sourceArray, targetArray; |  | ||||||
| 
 |  | ||||||
|         // Determine source and target arrays |  | ||||||
|         if (sourceGroupIndex === -1) { |  | ||||||
|             sourceArray = unassignedStudents.value; |  | ||||||
|         } else { |  | ||||||
|             sourceArray = currentGroups.value[sourceGroupIndex]; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         if (targetGroupIndex === -1) { |  | ||||||
|             targetArray = unassignedStudents.value; |  | ||||||
|         } else { |  | ||||||
|             targetArray = currentGroups.value[targetGroupIndex]; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // Remove from source |  | ||||||
|     const [movedStudent] = sourceArray.splice(sourceStudentIndex, 1); |     const [movedStudent] = sourceArray.splice(sourceStudentIndex, 1); | ||||||
| 
 |  | ||||||
|         // Add to target |  | ||||||
|     if (targetStudentIndex !== undefined) { |     if (targetStudentIndex !== undefined) { | ||||||
|         targetArray.splice(targetStudentIndex, 0, movedStudent); |         targetArray.splice(targetStudentIndex, 0, movedStudent); | ||||||
|     } else { |     } else { | ||||||
|  | @ -161,10 +326,9 @@ | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     draggedItem.value = null; |     draggedItem.value = null; | ||||||
|     } | } | ||||||
| 
 | 
 | ||||||
| 
 | function saveDragDrop(): void { | ||||||
|     function saveDragDrop() { |  | ||||||
|     if (unassignedStudents.value.length > 0) { |     if (unassignedStudents.value.length > 0) { | ||||||
|         alert(t("please-assign-all-students")); |         alert(t("please-assign-all-students")); | ||||||
|         return; |         return; | ||||||
|  | @ -177,67 +341,48 @@ | ||||||
|     activeDialog.value = null; |     activeDialog.value = null; | ||||||
|     emit("done"); |     emit("done"); | ||||||
|     emit("close"); |     emit("close"); | ||||||
|     } | } | ||||||
| 
 | 
 | ||||||
|     // Preview current groups in the main view | const showGroupsPreview = computed(() => { | ||||||
|     const showGroupsPreview = computed(() => { |  | ||||||
|     return currentGroups.value.length > 0 || unassignedStudents.value.length > 0; |     return currentGroups.value.length > 0 || unassignedStudents.value.length > 0; | ||||||
|     }); | }); | ||||||
| 
 | 
 | ||||||
|     function removeStudent(groupIndex, student) { | function removeStudent(groupIndex: number, student: StudentItem): void { | ||||||
|     const group = currentGroups.value[groupIndex]; |     const group = currentGroups.value[groupIndex]; | ||||||
|     currentGroups.value[groupIndex] = group.filter((s) => s.username !== student.username); |     currentGroups.value[groupIndex] = group.filter((s) => s.username !== student.username); | ||||||
|     unassignedStudents.value.push(student); |     unassignedStudents.value.push(student); | ||||||
|     } | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|     <v-card class="pa-4"> |     <v-card class="pa-4"> | ||||||
|         <!-- Current Groups Preview --> |         <!-- Current groups and unassigned students Preview --> | ||||||
|         <div |         <div v-if="showGroupsPreview" class="mb-6"> | ||||||
|             v-if="showGroupsPreview" |  | ||||||
|             class="mb-6" |  | ||||||
|         > |  | ||||||
|             <h3 class="mb-2">{{ t("current-groups") }}</h3> |             <h3 class="mb-2">{{ t("current-groups") }}</h3> | ||||||
|             <div |             <div> | ||||||
|             > |  | ||||||
|                 <div class="d-flex flex-wrap"> |                 <div class="d-flex flex-wrap"> | ||||||
|                     <label>{{currentGroups.length}}</label> |                     <label>{{ currentGroups.length }}</label> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
| 
 | 
 | ||||||
|             <div |             <div v-if="unassignedStudents.length > 0" class="mt-3"> | ||||||
|                 v-if="unassignedStudents.length > 0" |  | ||||||
|                 class="mt-3" |  | ||||||
|             > |  | ||||||
|                 <strong>{{ t("unassigned-students") }}:</strong> |                 <strong>{{ t("unassigned-students") }}:</strong> | ||||||
|                 <div class="d-flex flex-wrap"> |                 <div class="d-flex flex-wrap"> | ||||||
|                     <label>{{unassignedStudents.length}}</label> |                     <label>{{ unassignedStudents.length }}</label> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <!-- Action Buttons --> |         <v-row justify="center" class="mb-4"> | ||||||
|         <v-row |             <v-btn color="primary" @click="activeDialog = 'random'"> | ||||||
|             justify="center" |  | ||||||
|             class="mb-4" |  | ||||||
|         > |  | ||||||
|             <v-btn |  | ||||||
|                 color="primary" |  | ||||||
|                 @click="activeDialog = 'random'" |  | ||||||
|             > |  | ||||||
|                 {{ t("randomly-create-groups") }} |                 {{ t("randomly-create-groups") }} | ||||||
|             </v-btn> |             </v-btn> | ||||||
|             <v-btn |             <v-btn color="secondary" class="ml-4" @click="activeDialog = 'dragdrop'"> | ||||||
|                 color="secondary" |  | ||||||
|                 class="ml-4" |  | ||||||
|                 @click="activeDialog = 'dragdrop'" |  | ||||||
|             > |  | ||||||
|                 {{ t("drag-and-drop") }} |                 {{ t("drag-and-drop") }} | ||||||
|             </v-btn> |             </v-btn> | ||||||
|         </v-row> |         </v-row> | ||||||
| 
 | 
 | ||||||
|         <!-- Random Groups Dialog --> |         <!-- Random Groups selection Dialog --> | ||||||
|         <v-dialog |         <v-dialog | ||||||
|             :model-value="activeDialog === 'random'" |             :model-value="activeDialog === 'random'" | ||||||
|             @update:model-value="(val) => (val ? (activeDialog = 'random') : (activeDialog = null))" |             @update:model-value="(val) => (val ? (activeDialog = 'random') : (activeDialog = null))" | ||||||
|  | @ -298,12 +443,8 @@ | ||||||
|                 </v-card-text> |                 </v-card-text> | ||||||
| 
 | 
 | ||||||
|                 <v-card-actions> |                 <v-card-actions> | ||||||
|                     <v-spacer /> |                     <v-spacer/> | ||||||
|                     <v-btn |                     <v-btn text @click="activeDialog = null">{{ t("cancel") }}</v-btn> | ||||||
|                         text |  | ||||||
|                         @click="activeDialog = null" |  | ||||||
|                         >{{ t("cancel") }}</v-btn |  | ||||||
|                     > |  | ||||||
|                     <v-btn |                     <v-btn | ||||||
|                         color="success" |                         color="success" | ||||||
|                         @click="saveRandomGroups" |                         @click="saveRandomGroups" | ||||||
|  | @ -356,7 +497,10 @@ | ||||||
|                                             :key="student.username" |                                             :key="student.username" | ||||||
|                                             class="draggable-item ma-1" |                                             class="draggable-item ma-1" | ||||||
|                                             draggable="true" |                                             draggable="true" | ||||||
|                                             @dragstart="handleDragStart(groupIndex, studentIndex)" |                                             @touchstart="handleTouchStart($event, groupIndex, studentIndex)" | ||||||
|  |                                             @touchmove="handleTouchMove($event)" | ||||||
|  |                                             @touchend="handleTouchEnd($event)" | ||||||
|  |                                             @dragstart="handleDragStart($event, groupIndex, studentIndex)" | ||||||
|                                             @dragover.prevent="handleDragOver($event, groupIndex)" |                                             @dragover.prevent="handleDragOver($event, groupIndex)" | ||||||
|                                             @drop="handleDrop($event, groupIndex, studentIndex)" |                                             @drop="handleDrop($event, groupIndex, studentIndex)" | ||||||
|                                         > |                                         > | ||||||
|  | @ -387,7 +531,10 @@ | ||||||
|                                     :key="student.username" |                                     :key="student.username" | ||||||
|                                     class="draggable-item ma-1" |                                     class="draggable-item ma-1" | ||||||
|                                     draggable="true" |                                     draggable="true" | ||||||
|                                     @dragstart="handleDragStart(-1, studentIndex)" |                                     @touchstart="handleTouchStart($event, -1, studentIndex)" | ||||||
|  |                                     @touchmove="handleTouchMove($event)" | ||||||
|  |                                     @touchend="handleTouchEnd($event)" | ||||||
|  |                                     @dragstart="handleDragStart($event, -1, studentIndex)" | ||||||
|                                     @dragover.prevent="handleDragOver($event, -1)" |                                     @dragover.prevent="handleDragOver($event, -1)" | ||||||
|                                     @drop="handleDrop($event, -1, studentIndex)" |                                     @drop="handleDrop($event, -1, studentIndex)" | ||||||
|                                 > |                                 > | ||||||
|  | @ -399,7 +546,7 @@ | ||||||
|                 </v-card-text> |                 </v-card-text> | ||||||
| 
 | 
 | ||||||
|                 <v-card-actions> |                 <v-card-actions> | ||||||
|                     <v-spacer /> |                     <v-spacer/> | ||||||
|                     <v-btn text @click="activeDialog = null">{{ t("cancel") }}</v-btn> |                     <v-btn text @click="activeDialog = null">{{ t("cancel") }}</v-btn> | ||||||
|                     <v-btn |                     <v-btn | ||||||
|                         color="primary" |                         color="primary" | ||||||
|  | @ -415,16 +562,45 @@ | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
|     .group-box { | .group-box { | ||||||
|         min-height: 150px; |     min-height: 100px; | ||||||
|         max-height: 300px; |     max-height: 200px; | ||||||
|     overflow-y: auto; |     overflow-y: auto; | ||||||
|     background-color: #fafafa; |     background-color: #fafafa; | ||||||
|     border-radius: 4px; |     border-radius: 4px; | ||||||
|     } |     transition: all 0.2s; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
|     .v-expansion-panel-text { | .group-box.highlight { | ||||||
|  |     background-color: #e3f2fd; | ||||||
|  |     border: 2px dashed #2196f3; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .v-expansion-panel-text { | ||||||
|     max-height: 200px; |     max-height: 200px; | ||||||
|     overflow-y: auto; |     overflow-y: auto; | ||||||
|     } | } | ||||||
|  | 
 | ||||||
|  | .drag-clone { | ||||||
|  |     z-index: 10000; | ||||||
|  |     transform: scale(1.05); | ||||||
|  |     box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); | ||||||
|  |     transition: transform 0.1s; | ||||||
|  |     will-change: transform; | ||||||
|  |     pointer-events: none; | ||||||
|  |     display: inline-flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     border-radius: 16px; | ||||||
|  |     background-color: inherit; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .draggable-item { | ||||||
|  |     display: inline-block; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .draggable-item .v-chip[style*="hidden"] { | ||||||
|  |     visibility: hidden; | ||||||
|  |     display: inline-block; | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|  |  | ||||||
|  | @ -344,6 +344,7 @@ async function saveChanges(): Promise<void> { | ||||||
|                         md="6" |                         md="6" | ||||||
|                         class="responsive-col" |                         class="responsive-col" | ||||||
|                     > |                     > | ||||||
|  |                         <div class="table-container"> | ||||||
|                             <v-table class="table"> |                             <v-table class="table"> | ||||||
|                                 <thead> |                                 <thead> | ||||||
|                                 <tr> |                                 <tr> | ||||||
|  | @ -407,6 +408,7 @@ async function saveChanges(): Promise<void> { | ||||||
|                                 </tr> |                                 </tr> | ||||||
|                                 </tbody> |                                 </tbody> | ||||||
|                             </v-table> |                             </v-table> | ||||||
|  |                         </div> | ||||||
|                     </v-col> |                     </v-col> | ||||||
|                 </v-row> |                 </v-row> | ||||||
|                 <v-dialog |                 <v-dialog | ||||||
|  | @ -510,6 +512,17 @@ main { | ||||||
|     margin-left: 30px; |     margin-left: 30px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .table-container { | ||||||
|  |     width: 100%; | ||||||
|  |     overflow-x: visible; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .table { | ||||||
|  |     width: 100%; | ||||||
|  |     min-width: auto; | ||||||
|  |     table-layout: auto; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| @media screen and (max-width: 850px) { | @media screen and (max-width: 850px) { | ||||||
|     h1 { |     h1 { | ||||||
|         text-align: center; |         text-align: center; | ||||||
|  | @ -540,6 +553,12 @@ main { | ||||||
| 
 | 
 | ||||||
|     .table { |     .table { | ||||||
|         width: 100%; |         width: 100%; | ||||||
|  |         display: block; | ||||||
|  |         overflow-x: auto; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .table-container { | ||||||
|  |         overflow-x: auto; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     .responsive-col { |     .responsive-col { | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -101,6 +101,7 @@ | ||||||
|                 "@tanstack/vue-query": "^5.69.0", |                 "@tanstack/vue-query": "^5.69.0", | ||||||
|                 "@vueuse/core": "^13.1.0", |                 "@vueuse/core": "^13.1.0", | ||||||
|                 "axios": "^1.8.2", |                 "axios": "^1.8.2", | ||||||
|  |                 "interactjs": "^1.10.27", | ||||||
|                 "oidc-client-ts": "^3.1.0", |                 "oidc-client-ts": "^3.1.0", | ||||||
|                 "rollup": "^4.40.0", |                 "rollup": "^4.40.0", | ||||||
|                 "uuid": "^11.1.0", |                 "uuid": "^11.1.0", | ||||||
|  | @ -1596,6 +1597,12 @@ | ||||||
|                 "url": "https://github.com/sponsors/nzakas" |                 "url": "https://github.com/sponsors/nzakas" | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/@interactjs/types": { | ||||||
|  |             "version": "1.10.27", | ||||||
|  |             "resolved": "https://registry.npmjs.org/@interactjs/types/-/types-1.10.27.tgz", | ||||||
|  |             "integrity": "sha512-BUdv0cvs4H5ODuwft2Xp4eL8Vmi3LcihK42z0Ft/FbVJZoRioBsxH+LlsBdK4tAie7PqlKGy+1oyOncu1nQ6eA==", | ||||||
|  |             "license": "MIT" | ||||||
|  |         }, | ||||||
|         "node_modules/@intlify/core-base": { |         "node_modules/@intlify/core-base": { | ||||||
|             "version": "11.1.3", |             "version": "11.1.3", | ||||||
|             "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.3.tgz", |             "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.3.tgz", | ||||||
|  | @ -6830,6 +6837,15 @@ | ||||||
|             "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", |             "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", | ||||||
|             "license": "ISC" |             "license": "ISC" | ||||||
|         }, |         }, | ||||||
|  |         "node_modules/interactjs": { | ||||||
|  |             "version": "1.10.27", | ||||||
|  |             "resolved": "https://registry.npmjs.org/interactjs/-/interactjs-1.10.27.tgz", | ||||||
|  |             "integrity": "sha512-y/8RcCftGAF24gSp76X2JS3XpHiUvDQyhF8i7ujemBz77hwiHDuJzftHx7thY8cxGogwGiPJ+o97kWB6eAXnsA==", | ||||||
|  |             "license": "MIT", | ||||||
|  |             "dependencies": { | ||||||
|  |                 "@interactjs/types": "1.10.27" | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|         "node_modules/interpret": { |         "node_modules/interpret": { | ||||||
|             "version": "2.2.0", |             "version": "2.2.0", | ||||||
|             "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", |             "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", | ||||||
|  |  | ||||||
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana