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",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"axios": "^1.8.2",
|
||||
"interactjs": "^1.10.27",
|
||||
"oidc-client-ts": "^3.1.0",
|
||||
"rollup": "^4.40.0",
|
||||
"uuid": "^11.1.0",
|
||||
|
|
|
@ -1,243 +1,388 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useClassStudentsQuery } from "@/queries/classes";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {useClassStudentsQuery} from "@/queries/classes";
|
||||
|
||||
const props = defineProps<{
|
||||
classId: string | undefined;
|
||||
groups: object[];
|
||||
}>();
|
||||
const emit = defineEmits(["close", "groupsUpdated", "done"]);
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
classId: string | undefined;
|
||||
groups: object[];
|
||||
}>();
|
||||
const emit = defineEmits(["close", "groupsUpdated", "done"]);
|
||||
const {t} = useI18n();
|
||||
|
||||
interface StudentItem {
|
||||
username: string;
|
||||
fullName: string;
|
||||
}
|
||||
interface StudentItem {
|
||||
username: string;
|
||||
fullName: string;
|
||||
}
|
||||
|
||||
const { data: studentsData } = useClassStudentsQuery(() => props.classId, true);
|
||||
const {data: studentsData} = useClassStudentsQuery(() => props.classId, true);
|
||||
|
||||
// Dialog states
|
||||
const activeDialog = ref<"random" | "dragdrop" | null>(null);
|
||||
// Dialog states for group editing
|
||||
const activeDialog = ref<"random" | "dragdrop" | null>(null);
|
||||
|
||||
// Drag state
|
||||
const draggedItem = ref<{groupIndex: number, studentIndex: number} | null>(null);
|
||||
// Drag state for the drag and drop
|
||||
const draggedItem = ref<{ groupIndex: number, studentIndex: number } | null>(null);
|
||||
|
||||
const currentGroups = ref<StudentItem[][]>([]);
|
||||
const unassignedStudents = ref<StudentItem[]>([]);
|
||||
const allStudents = ref<StudentItem[]>([]);
|
||||
const currentGroups = ref<StudentItem[][]>([]);
|
||||
const unassignedStudents = ref<StudentItem[]>([]);
|
||||
const allStudents = ref<StudentItem[]>([]);
|
||||
|
||||
// Random groups state
|
||||
const groupSize = ref(1);
|
||||
const randomGroupsPreview = ref<StudentItem[][]>([]);
|
||||
// Random groups state
|
||||
const groupSize = ref(1);
|
||||
const randomGroupsPreview = ref<StudentItem[][]>([]);
|
||||
|
||||
// Initialize data
|
||||
watch(
|
||||
() => [studentsData.value, props.groups],
|
||||
([studentsVal, existingGroups]) => {
|
||||
if (!studentsVal) return;
|
||||
// Initialize data
|
||||
watch(
|
||||
() => [studentsData.value, props.groups],
|
||||
([studentsVal, existingGroups]) => {
|
||||
if (!studentsVal) return;
|
||||
|
||||
// Initialize all students
|
||||
allStudents.value = studentsVal.students.map((s) => ({
|
||||
username: s.username,
|
||||
fullName: `${s.firstName} ${s.lastName}`,
|
||||
}));
|
||||
// Initialize all students
|
||||
allStudents.value = studentsVal.students.map((s) => ({
|
||||
username: s.username,
|
||||
fullName: `${s.firstName} ${s.lastName}`,
|
||||
}));
|
||||
|
||||
// Initialize groups if they exist
|
||||
if (existingGroups && existingGroups.length > 0) {
|
||||
currentGroups.value = existingGroups.map((group) =>
|
||||
group.members.map(member => ({
|
||||
username: member.username,
|
||||
fullName: `${member.firstName} ${member.lastName}`
|
||||
}))
|
||||
);
|
||||
const assignedUsernames = new Set(
|
||||
existingGroups.flatMap((g) => g.members.map((m: StudentItem) => m.username)),
|
||||
);
|
||||
unassignedStudents.value = allStudents.value.filter((s) => !assignedUsernames.has(s.username));
|
||||
} else {
|
||||
// Default to all students unassigned
|
||||
currentGroups.value = [];
|
||||
unassignedStudents.value = [...allStudents.value];
|
||||
}
|
||||
|
||||
// Initialize random preview with current groups
|
||||
randomGroupsPreview.value = [...currentGroups.value];
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
|
||||
// Random groups functions
|
||||
function generateRandomGroups() {
|
||||
if (groupSize.value < 1) return;
|
||||
|
||||
// Shuffle students
|
||||
const shuffled = [...allStudents.value].sort(() => Math.random() - 0.5);
|
||||
|
||||
// Create new groups
|
||||
const newGroups: StudentItem[][] = [];
|
||||
const groupCount = Math.ceil(shuffled.length / groupSize.value);
|
||||
|
||||
for (let i = 0; i < groupCount; i++) {
|
||||
newGroups.push([]);
|
||||
}
|
||||
|
||||
// Distribute students
|
||||
shuffled.forEach((student, index) => {
|
||||
const groupIndex = index % groupCount;
|
||||
newGroups[groupIndex].push(student);
|
||||
});
|
||||
|
||||
randomGroupsPreview.value = newGroups;
|
||||
}
|
||||
|
||||
function saveRandomGroups() {
|
||||
if (randomGroupsPreview.value.length === 0) {
|
||||
alert(t("please-generate-groups-first"));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
"groupsUpdated",
|
||||
randomGroupsPreview.value.map((g) => g.map((s) => s.username)),
|
||||
);
|
||||
activeDialog.value = null;
|
||||
emit("done");
|
||||
emit("close");
|
||||
}
|
||||
|
||||
// Drag and drop functions
|
||||
function addNewGroup() {
|
||||
currentGroups.value.push([]);
|
||||
}
|
||||
|
||||
function removeGroup(index: number) {
|
||||
// Move students back to unassigned
|
||||
unassignedStudents.value.push(...currentGroups.value[index]);
|
||||
currentGroups.value.splice(index, 1);
|
||||
}
|
||||
|
||||
// Native Drag & Drop Handlers
|
||||
function handleDragStart(groupIndex: number, studentIndex: number) {
|
||||
draggedItem.value = { groupIndex, studentIndex };
|
||||
}
|
||||
|
||||
function handleDragOver(e: DragEvent, _: number) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer!.dropEffect = "move";
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent, targetGroupIndex: number, targetStudentIndex?: number) {
|
||||
e.preventDefault();
|
||||
if (!draggedItem.value) return;
|
||||
|
||||
const { groupIndex: sourceGroupIndex, studentIndex: sourceStudentIndex } = draggedItem.value;
|
||||
const isSameGroup = sourceGroupIndex === targetGroupIndex;
|
||||
|
||||
let sourceArray, targetArray;
|
||||
|
||||
// Determine source and target arrays
|
||||
if (sourceGroupIndex === -1) {
|
||||
sourceArray = unassignedStudents.value;
|
||||
// Initialize groups if they exist
|
||||
if (existingGroups && existingGroups.length > 0) {
|
||||
currentGroups.value = existingGroups.map((group) =>
|
||||
group.members.map(member => ({
|
||||
username: member.username,
|
||||
fullName: `${member.firstName} ${member.lastName}`
|
||||
}))
|
||||
);
|
||||
const assignedUsernames = new Set(
|
||||
existingGroups.flatMap((g) => g.members.map((m: StudentItem) => m.username)),
|
||||
);
|
||||
unassignedStudents.value = allStudents.value.filter((s) => !assignedUsernames.has(s.username));
|
||||
} else {
|
||||
sourceArray = currentGroups.value[sourceGroupIndex];
|
||||
currentGroups.value = [];
|
||||
unassignedStudents.value = [...allStudents.value];
|
||||
}
|
||||
|
||||
if (targetGroupIndex === -1) {
|
||||
targetArray = unassignedStudents.value;
|
||||
} else {
|
||||
targetArray = currentGroups.value[targetGroupIndex];
|
||||
}
|
||||
randomGroupsPreview.value = [...currentGroups.value];
|
||||
},
|
||||
{immediate: true},
|
||||
);
|
||||
|
||||
// Remove from source
|
||||
const [movedStudent] = sourceArray.splice(sourceStudentIndex, 1);
|
||||
/** Random groups functions */
|
||||
function generateRandomGroups(): void {
|
||||
if (groupSize.value < 1) return;
|
||||
|
||||
// Add to target
|
||||
if (targetStudentIndex !== undefined) {
|
||||
targetArray.splice(targetStudentIndex, 0, movedStudent);
|
||||
} else {
|
||||
targetArray.push(movedStudent);
|
||||
}
|
||||
// Shuffle students
|
||||
const shuffled = [...allStudents.value].sort(() => Math.random() - 0.5);
|
||||
|
||||
draggedItem.value = null;
|
||||
// Create new groups
|
||||
const newGroups: StudentItem[][] = [];
|
||||
const groupCount = Math.ceil(shuffled.length / groupSize.value);
|
||||
|
||||
for (let i = 0; i < groupCount; i++) {
|
||||
newGroups.push([]);
|
||||
}
|
||||
|
||||
|
||||
function saveDragDrop() {
|
||||
if (unassignedStudents.value.length > 0) {
|
||||
alert(t("please-assign-all-students"));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
"groupsUpdated",
|
||||
currentGroups.value.map((g) => g.map((s) => s.username)),
|
||||
);
|
||||
activeDialog.value = null;
|
||||
emit("done");
|
||||
emit("close");
|
||||
}
|
||||
|
||||
// Preview current groups in the main view
|
||||
const showGroupsPreview = computed(() => {
|
||||
return currentGroups.value.length > 0 || unassignedStudents.value.length > 0;
|
||||
// Distribute students
|
||||
shuffled.forEach((student, index) => {
|
||||
const groupIndex = index % groupCount;
|
||||
newGroups[groupIndex].push(student);
|
||||
});
|
||||
|
||||
function removeStudent(groupIndex, student) {
|
||||
const group = currentGroups.value[groupIndex];
|
||||
currentGroups.value[groupIndex] = group.filter((s) => s.username !== student.username);
|
||||
unassignedStudents.value.push(student);
|
||||
randomGroupsPreview.value = newGroups;
|
||||
}
|
||||
|
||||
function saveRandomGroups(): void {
|
||||
if (randomGroupsPreview.value.length === 0) {
|
||||
alert(t("please-generate-groups-first"));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
"groupsUpdated",
|
||||
randomGroupsPreview.value.map((g) => g.map((s) => s.username)),
|
||||
);
|
||||
activeDialog.value = null;
|
||||
emit("done");
|
||||
emit("close");
|
||||
}
|
||||
|
||||
function addNewGroup() {
|
||||
currentGroups.value.push([]);
|
||||
}
|
||||
|
||||
function removeGroup(index: number) {
|
||||
// Move students back to unassigned
|
||||
unassignedStudents.value.push(...currentGroups.value[index]);
|
||||
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;
|
||||
}
|
||||
|
||||
clone.style.left = `${touch.clientX - clone.offsetWidth / 2}px`;
|
||||
clone.style.top = `${touch.clientY - clone.offsetHeight / 2}px`;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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();
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent, targetGroupIndex: number, targetStudentIndex?: number): void {
|
||||
e.preventDefault();
|
||||
if (!draggedItem.value) return;
|
||||
|
||||
const {groupIndex: sourceGroupIndex, studentIndex: sourceStudentIndex} = draggedItem.value;
|
||||
const sourceArray = sourceGroupIndex === -1
|
||||
? unassignedStudents.value
|
||||
: currentGroups.value[sourceGroupIndex];
|
||||
const targetArray = targetGroupIndex === -1
|
||||
? unassignedStudents.value
|
||||
: currentGroups.value[targetGroupIndex];
|
||||
|
||||
const [movedStudent] = sourceArray.splice(sourceStudentIndex, 1);
|
||||
if (targetStudentIndex !== undefined) {
|
||||
targetArray.splice(targetStudentIndex, 0, movedStudent);
|
||||
} else {
|
||||
targetArray.push(movedStudent);
|
||||
}
|
||||
|
||||
draggedItem.value = null;
|
||||
}
|
||||
|
||||
function saveDragDrop(): void {
|
||||
if (unassignedStudents.value.length > 0) {
|
||||
alert(t("please-assign-all-students"));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
"groupsUpdated",
|
||||
currentGroups.value.map((g) => g.map((s) => s.username)),
|
||||
);
|
||||
activeDialog.value = null;
|
||||
emit("done");
|
||||
emit("close");
|
||||
}
|
||||
|
||||
const showGroupsPreview = computed(() => {
|
||||
return currentGroups.value.length > 0 || unassignedStudents.value.length > 0;
|
||||
});
|
||||
|
||||
function removeStudent(groupIndex: number, student: StudentItem): void {
|
||||
const group = currentGroups.value[groupIndex];
|
||||
currentGroups.value[groupIndex] = group.filter((s) => s.username !== student.username);
|
||||
unassignedStudents.value.push(student);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card class="pa-4">
|
||||
<!-- Current Groups Preview -->
|
||||
<div
|
||||
v-if="showGroupsPreview"
|
||||
class="mb-6"
|
||||
>
|
||||
<!-- Current groups and unassigned students Preview -->
|
||||
<div v-if="showGroupsPreview" class="mb-6">
|
||||
<h3 class="mb-2">{{ t("current-groups") }}</h3>
|
||||
<div
|
||||
>
|
||||
<div>
|
||||
<div class="d-flex flex-wrap">
|
||||
<label>{{currentGroups.length}}</label>
|
||||
<label>{{ currentGroups.length }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="unassignedStudents.length > 0"
|
||||
class="mt-3"
|
||||
>
|
||||
<div v-if="unassignedStudents.length > 0" class="mt-3">
|
||||
<strong>{{ t("unassigned-students") }}:</strong>
|
||||
<div class="d-flex flex-wrap">
|
||||
<label>{{unassignedStudents.length}}</label>
|
||||
<label>{{ unassignedStudents.length }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<v-row
|
||||
justify="center"
|
||||
class="mb-4"
|
||||
>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="activeDialog = 'random'"
|
||||
>
|
||||
<v-row justify="center" class="mb-4">
|
||||
<v-btn color="primary" @click="activeDialog = 'random'">
|
||||
{{ t("randomly-create-groups") }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="secondary"
|
||||
class="ml-4"
|
||||
@click="activeDialog = 'dragdrop'"
|
||||
>
|
||||
<v-btn color="secondary" class="ml-4" @click="activeDialog = 'dragdrop'">
|
||||
{{ t("drag-and-drop") }}
|
||||
</v-btn>
|
||||
</v-row>
|
||||
|
||||
<!-- Random Groups Dialog -->
|
||||
<!-- Random Groups selection Dialog -->
|
||||
<v-dialog
|
||||
:model-value="activeDialog === 'random'"
|
||||
@update:model-value="(val) => (val ? (activeDialog = 'random') : (activeDialog = null))"
|
||||
|
@ -298,12 +443,8 @@
|
|||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@click="activeDialog = null"
|
||||
>{{ t("cancel") }}</v-btn
|
||||
>
|
||||
<v-spacer/>
|
||||
<v-btn text @click="activeDialog = null">{{ t("cancel") }}</v-btn>
|
||||
<v-btn
|
||||
color="success"
|
||||
@click="saveRandomGroups"
|
||||
|
@ -356,7 +497,10 @@
|
|||
:key="student.username"
|
||||
class="draggable-item ma-1"
|
||||
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)"
|
||||
@drop="handleDrop($event, groupIndex, studentIndex)"
|
||||
>
|
||||
|
@ -387,7 +531,10 @@
|
|||
:key="student.username"
|
||||
class="draggable-item ma-1"
|
||||
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)"
|
||||
@drop="handleDrop($event, -1, studentIndex)"
|
||||
>
|
||||
|
@ -399,7 +546,7 @@
|
|||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-spacer/>
|
||||
<v-btn text @click="activeDialog = null">{{ t("cancel") }}</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
|
@ -415,16 +562,45 @@
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
.group-box {
|
||||
min-height: 150px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background-color: #fafafa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.group-box {
|
||||
min-height: 100px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background-color: #fafafa;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.v-expansion-panel-text {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.group-box.highlight {
|
||||
background-color: #e3f2fd;
|
||||
border: 2px dashed #2196f3;
|
||||
}
|
||||
|
||||
.v-expansion-panel-text {
|
||||
max-height: 200px;
|
||||
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>
|
||||
|
|
|
@ -344,69 +344,71 @@ async function saveChanges(): Promise<void> {
|
|||
md="6"
|
||||
class="responsive-col"
|
||||
>
|
||||
<v-table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header">{{ t("group") }}</th>
|
||||
<th class="header">{{ t("progress") }}</th>
|
||||
<th class="header">{{ t("submission") }}</th>
|
||||
<th class="header">
|
||||
<v-btn
|
||||
@click="editGroups = true"
|
||||
variant="text"
|
||||
>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="g in allGroups"
|
||||
:key="g.originalGroupNo"
|
||||
>
|
||||
<td>
|
||||
<v-btn
|
||||
@click="openGroupDetails(g)"
|
||||
variant="text"
|
||||
>
|
||||
{{ g.name }}
|
||||
<v-icon end>mdi-menu-right</v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
<div class="table-container">
|
||||
<v-table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header">{{ t("group") }}</th>
|
||||
<th class="header">{{ t("progress") }}</th>
|
||||
<th class="header">{{ t("submission") }}</th>
|
||||
<th class="header">
|
||||
<v-btn
|
||||
@click="editGroups = true"
|
||||
variant="text"
|
||||
>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="g in allGroups"
|
||||
:key="g.originalGroupNo"
|
||||
>
|
||||
<td>
|
||||
<v-btn
|
||||
@click="openGroupDetails(g)"
|
||||
variant="text"
|
||||
>
|
||||
{{ g.name }}
|
||||
<v-icon end>mdi-menu-right</v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<GroupProgressRow
|
||||
:group-number="g.originalGroupNo"
|
||||
:learning-path="learningPath"
|
||||
:language="lang"
|
||||
:assignment-id="assignmentId"
|
||||
:class-id="classId"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<GroupProgressRow
|
||||
:group-number="g.originalGroupNo"
|
||||
:learning-path="learningPath"
|
||||
:language="lang"
|
||||
:assignment-id="assignmentId"
|
||||
:class-id="classId"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<GroupSubmissionStatus
|
||||
:group="g"
|
||||
:assignment-id="assignmentId"
|
||||
:class-id="classId"
|
||||
:language="lang"
|
||||
:go-to-group-submission-link="goToGroupSubmissionLink"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<GroupSubmissionStatus
|
||||
:group="g"
|
||||
:assignment-id="assignmentId"
|
||||
:class-id="classId"
|
||||
:language="lang"
|
||||
:go-to-group-submission-link="goToGroupSubmissionLink"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<!-- Edit icon -->
|
||||
<td>
|
||||
<v-btn
|
||||
@click=""
|
||||
variant="text"
|
||||
>
|
||||
<v-icon color="red">mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
<!-- Edit icon -->
|
||||
<td>
|
||||
<v-btn
|
||||
@click=""
|
||||
variant="text"
|
||||
>
|
||||
<v-icon color="red">mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-dialog
|
||||
|
@ -510,6 +512,17 @@ main {
|
|||
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) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
|
@ -540,6 +553,12 @@ main {
|
|||
|
||||
.table {
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.responsive-col {
|
||||
|
|
16
package-lock.json
generated
16
package-lock.json
generated
|
@ -101,6 +101,7 @@
|
|||
"@tanstack/vue-query": "^5.69.0",
|
||||
"@vueuse/core": "^13.1.0",
|
||||
"axios": "^1.8.2",
|
||||
"interactjs": "^1.10.27",
|
||||
"oidc-client-ts": "^3.1.0",
|
||||
"rollup": "^4.40.0",
|
||||
"uuid": "^11.1.0",
|
||||
|
@ -1596,6 +1597,12 @@
|
|||
"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": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.3.tgz",
|
||||
|
@ -6830,6 +6837,15 @@
|
|||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"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": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue