feat: add full screen mode

This commit is contained in:
2025-08-06 07:40:56 +02:00
parent 1a281d32d0
commit 72c7ab9a03

View File

@@ -23,7 +23,7 @@
<div><kbd class="bg-gray-700 px-1 rounded">F1</kbd> : Afficher/Masquer cette aide</div>
</div>
</div>
<div class="space-y-6">
<div class="w-full max-w-none space-y-6">
<div class="flex justify-between items-center">
<div>
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-blue-600 hover:text-blue-800 text-sm font-medium mb-2 inline-block">
@@ -34,29 +34,6 @@
</div>
</div>
{% if not grading_elements %}
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">Aucun élément de notation</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>Cette évaluation n'a pas encore d'éléments de notation configurés. Vous devez d'abord créer des exercices et leurs éléments de notation.</p>
</div>
<div class="mt-4">
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-sm font-medium text-yellow-800 underline hover:text-yellow-900">
Configurer l'évaluation →
</a>
</div>
</div>
</div>
</div>
{% else %}
<form method="POST" action="{{ url_for('grading.save_grades', assessment_id=assessment.id) }}" class="space-y-6" id="grading-form">
<!-- Guide de saisie unifié moderne -->
<div class="bg-gradient-to-r from-blue-50 to-purple-50 border border-blue-200 rounded-lg p-4">
<div class="flex justify-between items-start">
@@ -88,7 +65,44 @@
</span>
</div>
</div>
<div class="text-right flex items-center space-x-3">
</div>
</div>
{% if not grading_elements %}
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">Aucun élément de notation</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>Cette évaluation n'a pas encore d'éléments de notation configurés. Vous devez d'abord créer des exercices et leurs éléments de notation.</p>
</div>
<div class="mt-4">
<a href="{{ url_for('assessments.detail', id=assessment.id) }}" class="text-sm font-medium text-yellow-800 underline hover:text-yellow-900">
Configurer l'évaluation →
</a>
</div>
</div>
</div>
</div>
{% else %}
<form method="POST" action="{{ url_for('grading.save_grades', assessment_id=assessment.id) }}" class="space-y-6" id="grading-form">
<!-- Tableau de saisie -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-3 border-b border-gray-200 flex justify-between items-center">
<h2 class="text-base font-medium text-gray-900">Grille de notation</h2>
<div class="flex items-center space-x-3">
<button type="button" onclick="toggleFullscreen()" id="fullscreen-btn" class="text-xs bg-purple-100 hover:bg-purple-200 text-purple-800 px-3 py-1 rounded transition-colors flex items-center space-x-1" title="Passer en plein écran">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"></path>
</svg>
<span>Plein écran</span>
</button>
<button type="button" onclick="toggleKeyboardHelp()" class="text-xs bg-blue-100 hover:bg-blue-200 text-blue-800 px-2 py-1 rounded transition-colors">
📋 F1
</button>
@@ -98,49 +112,11 @@
</div>
</div>
</div>
</div>
<!-- Filtre des élèves -->
<div class="bg-white shadow rounded-lg overflow-hidden mb-4">
<div class="px-4 py-3 border-b border-gray-200">
<div class="flex items-center justify-between">
<h3 class="text-sm font-medium text-gray-700">Filtrer les élèves</h3>
<span id="student-count" class="text-xs text-gray-500">{{ students|length }} élève(s)</span>
</div>
</div>
<div class="p-4">
<div class="relative">
<input type="text"
id="student-filter"
placeholder="Rechercher un élève par nom ou prénom..."
class="w-full px-3 py-2 pl-10 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
</svg>
</div>
<button type="button"
id="clear-filter"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-400 hover:text-gray-600 hidden"
onclick="clearStudentFilter()">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
<!-- Tableau de saisie -->
<div class="bg-white shadow rounded-lg overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200">
<h2 class="text-base font-medium text-gray-900">Grille de notation</h2>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 table-fixed">
<!-- En-têtes des exercices -->
<thead class="bg-gradient-to-r from-indigo-50 to-purple-50">
<div class="grading-table-container">
<table class="min-w-full divide-y divide-gray-200 table-fixed grading-table">
<!-- En-têtes des exercices -->
<thead class="bg-gradient-to-r from-indigo-50 to-purple-50 sticky top-0 z-50 shadow-sm">
<tr class="border-b-2 border-indigo-200">
<th scope="col" class="px-3 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sticky left-0 bg-gradient-to-r from-indigo-50 to-purple-50 border-r border-indigo-200 w-48">
Élève
@@ -164,10 +140,32 @@
</thead>
<!-- En-têtes des éléments de notation -->
<thead class="bg-gray-50">
<thead class="bg-gray-50 sticky z-40 shadow-sm" style="top: 3rem;">
<tr>
<th scope="col" class="px-3 py-1.5 text-left text-xs font-medium text-gray-500 uppercase tracking-wider sticky left-0 bg-gray-50 border-r border-gray-200 w-48">
<!-- Vide pour alignement -->
<!-- Filtre des élèves intégré -->
<div class="relative w-full">
<input type="text"
id="student-filter"
placeholder="Filtrer les élèves..."
class="w-full px-2 py-1 pl-6 text-xs border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-white">
<div class="absolute inset-y-0 left-0 pl-2 flex items-center pointer-events-none">
<svg class="h-3 w-3 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
</svg>
</div>
<button type="button"
id="clear-filter"
class="absolute inset-y-0 right-0 pr-2 flex items-center text-gray-400 hover:text-gray-600 hidden"
onclick="clearStudentFilter()">
<svg class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
<div class="text-center mt-1">
<span id="student-count" class="text-xs text-gray-500">{{ students|length }} élève(s)</span>
</div>
</th>
{% set current_exercise = '' %}
{% for element in grading_elements %}
@@ -309,6 +307,61 @@
</div>
</form>
<!-- CSS custom pour forcer le sticky et pleine largeur -->
<style>
/* Pleine largeur pour cette page */
/* Mode plein écran */
.fullscreen-mode {
overflow: hidden !important;
}
.fullscreen-overlay {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 9999 !important;
background: white !important;
padding: 1rem !important;
overflow: hidden !important;
box-sizing: border-box !important;
}
.fullscreen-overlay .grading-table-container {
flex: 1 !important;
min-height: 0 !important;
overflow-x: auto !important;
overflow-y: auto !important;
}
.grading-table-container {
overflow-x: auto;
max-height: 100vh;
overflow-y: auto;
}
.grading-table thead {
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 50;
background: white;
}
.grading-table thead:nth-child(2) {
top: 3rem;
z-index: 40;
background: white;
}
.grading-table th[scope="col"]:first-child {
position: -webkit-sticky;
position: sticky;
left: 0;
z-index: 51;
background: white;
}
</style>
<!-- Légende -->
{% endif %}
</div>
@@ -409,7 +462,11 @@ function setupKeyboardNavigation() {
// Ctrl+S : Sauvegarder
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
saveForm();
if (isFullscreen) {
saveFormFromFullscreen();
} else {
saveForm();
}
}
// Ctrl+Z : Annuler dernière modification
@@ -785,6 +842,75 @@ function saveForm() {
}, 500);
}
// Sauvegarder depuis le mode plein écran
function saveFormFromFullscreen() {
const form = document.getElementById('grading-form');
const saveButtonFs = document.getElementById('save-button-fs');
const saveTextFs = document.getElementById('save-text-fs');
const saveSpinnerFs = document.getElementById('save-spinner-fs');
if (!form) {
console.error('Formulaire non trouvé');
showToast('Erreur : formulaire non trouvé', 'error');
return;
}
// Animation de sauvegarde en mode plein écran
if (saveButtonFs) {
saveButtonFs.disabled = true;
if (saveTextFs) saveTextFs.textContent = 'Sauvegarde...';
if (saveSpinnerFs) saveSpinnerFs.classList.remove('hidden');
}
// Synchroniser les données du mode plein écran vers le formulaire original
syncDataFromFullscreenToOriginal();
// Soumettre le formulaire original
setTimeout(() => {
form.submit();
}, 500);
}
// Synchroniser les données entre le mode plein écran et le formulaire original
function syncDataFromFullscreenToOriginal() {
if (!isFullscreen || !fullscreenOverlay) return;
// Synchroniser tous les champs de saisie
const fsInputs = fullscreenOverlay.querySelectorAll('.grading-input, .comment-input');
fsInputs.forEach(fsInput => {
const name = fsInput.name;
if (name) {
const originalInput = document.querySelector(`[name="${name}"]`);
if (originalInput) {
originalInput.value = fsInput.value;
}
}
});
console.log('Données synchronisées du plein écran vers le formulaire original');
}
// Synchroniser les données du formulaire original vers le mode plein écran
function syncDataFromOriginalToFullscreen() {
if (!isFullscreen || !fullscreenOverlay) return;
// Synchroniser tous les champs de saisie
const originalInputs = document.querySelectorAll('#grading-form .grading-input, #grading-form .comment-input');
originalInputs.forEach(originalInput => {
const name = originalInput.name;
if (name) {
const fsInput = fullscreenOverlay.querySelector(`[name="${name}"]`);
if (fsInput) {
fsInput.value = originalInput.value;
}
}
});
console.log('Données synchronisées du formulaire original vers le plein écran');
}
// Afficher/Masquer l'aide clavier
function toggleKeyboardHelp() {
const help = document.getElementById('keyboard-help');
@@ -1031,6 +1157,192 @@ function clearStudentFilter() {
}
}
// Gestion du mode plein écran
let isFullscreen = false;
let fullscreenOverlay = null;
function toggleFullscreen() {
const btn = document.getElementById('fullscreen-btn');
const btnText = btn.querySelector('span');
const btnIcon = btn.querySelector('svg path');
const tableContainer = document.querySelector('.bg-white.shadow.rounded-lg');
if (!isFullscreen) {
// Créer l'overlay plein écran
fullscreenOverlay = document.createElement('div');
fullscreenOverlay.className = 'fullscreen-overlay';
// Créer la structure plein écran optimisée
fullscreenOverlay.innerHTML = `
<div class="h-full flex flex-col justify-between">
<!-- Header avec contrôles -->
<div class="bg-white shadow rounded-t-lg border-b border-gray-200">
<div class="px-4 py-3 flex justify-between items-center">
<h2 class="text-base font-medium text-gray-900">Grille de notation</h2>
<div class="flex items-center space-x-3">
<button type="button" onclick="toggleFullscreen()" class="text-xs bg-red-100 hover:bg-red-200 text-red-800 px-3 py-1 rounded transition-colors flex items-center space-x-1" title="Quitter le plein écran">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 3v3a2 2 0 01-2 2H3m18 0h-3a2 2 0 01-2-2V3m0 18v-3a2 2 0 012-2h3M3 16h3a2 2 0 012 2v3"></path>
</svg>
<span>Quitter</span>
</button>
<button type="button" onclick="toggleKeyboardHelp()" class="text-xs bg-blue-100 hover:bg-blue-200 text-blue-800 px-2 py-1 rounded transition-colors">
📋 F1
</button>
<div class="text-xs">
<div class="text-blue-700">Progression :</div>
<div id="progress-indicator-fs" class="font-semibold text-blue-900">0 / {{ (students|length * grading_elements|length) }} champs</div>
</div>
</div>
</div>
</div>
<!-- Tableau central extensible -->
<div class="flex-1 bg-white shadow-none grading-table-container" style="min-height: 0;">
<!-- Le contenu du tableau sera cloné ici -->
</div>
<!-- Footer de sauvegarde fixe en bas -->
<div class="bg-white shadow rounded-b-lg border-t border-gray-200">
<div class="px-6 py-4 bg-gray-50 flex justify-between items-center">
<div class="flex items-center space-x-4 text-sm text-gray-600">
<div id="save-status-fs" class="flex items-center">
<span class="w-2 h-2 bg-gray-400 rounded-full mr-2"></span>
<span>Non sauvegardé</span>
</div>
</div>
<div class="flex space-x-3">
<button type="button" onclick="resetForm()" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors">
Réinitialiser
</button>
<button type="button" onclick="saveFormFromFullscreen()" id="save-button-fs" class="px-4 py-2 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors flex items-center">
<span id="save-text-fs">Sauvegarder les notes</span>
<span id="save-spinner-fs" class="ml-2 w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin hidden"></span>
</button>
</div>
</div>
</div>
</div>
`;
// Cloner seulement le contenu du tableau (table)
const originalTable = tableContainer.querySelector('table');
const tableClone = originalTable.cloneNode(true);
const fsTableContainer = fullscreenOverlay.querySelector('.grading-table-container');
fsTableContainer.appendChild(tableClone);
// Synchroniser les valeurs actuelles du formulaire vers le clone
syncDataFromOriginalToFullscreen();
// Ajouter l'overlay au body
document.body.appendChild(fullscreenOverlay);
document.body.classList.add('fullscreen-mode');
// Masquer le conteneur original
tableContainer.style.display = 'none';
// Mettre à jour le bouton
btnText.textContent = 'Quitter';
btnIcon.setAttribute('d', 'M8 3v3a2 2 0 01-2 2H3m18 0h-3a2 2 0 01-2-2V3m0 18v-3a2 2 0 012-2h3M3 16h3a2 2 0 012 2v3');
btn.title = 'Quitter le plein écran';
btn.classList.remove('bg-purple-100', 'hover:bg-purple-200', 'text-purple-800');
btn.classList.add('bg-red-100', 'hover:bg-red-200', 'text-red-800');
// Réinitialiser les événements JavaScript sur la copie
setupFullscreenEvents();
isFullscreen = true;
} else {
// Supprimer l'overlay
if (fullscreenOverlay) {
document.body.removeChild(fullscreenOverlay);
fullscreenOverlay = null;
}
document.body.classList.remove('fullscreen-mode');
// Réafficher le conteneur original
const tableContainer = document.querySelector('.bg-white.shadow.rounded-lg');
tableContainer.style.display = '';
// Mettre à jour le bouton
btnText.textContent = 'Plein écran';
btnIcon.setAttribute('d', 'M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4');
btn.title = 'Passer en plein écran';
btn.classList.remove('bg-red-100', 'hover:bg-red-200', 'text-red-800');
btn.classList.add('bg-purple-100', 'hover:bg-purple-200', 'text-purple-800');
isFullscreen = false;
}
}
function setupFullscreenEvents() {
if (!fullscreenOverlay) return;
// Réattacher les événements nécessaires
const filterInput = fullscreenOverlay.querySelector('#student-filter');
if (filterInput) {
setupStudentFilterForOverlay(filterInput);
}
// Synchroniser l'indicateur de progression
const originalIndicator = document.getElementById('progress-indicator');
const fsIndicator = fullscreenOverlay.querySelector('#progress-indicator-fs');
if (originalIndicator && fsIndicator) {
fsIndicator.textContent = originalIndicator.textContent;
fsIndicator.className = originalIndicator.className;
}
// Réattacher les événements sur les champs de saisie en mode plein écran
const fsInputs = fullscreenOverlay.querySelectorAll('.grading-input, .comment-input');
fsInputs.forEach(input => {
if (input.classList.contains('grading-input')) {
// Événements pour les champs de notation
input.addEventListener('input', function() { handleGradeChange(this); });
input.addEventListener('change', function() { handleGradeChange(this); });
input.addEventListener('focus', function() { handleGradeFocus(this); });
input.addEventListener('keydown', function(e) { handleGradeKeydown(e, this); });
} else if (input.classList.contains('comment-input')) {
// Événements pour les commentaires
input.addEventListener('keydown', function(e) { handleCommentKeydown(e, this); });
}
});
// Appliquer les couleurs initiales aux champs pré-remplis
applyInitialColors();
}
function setupStudentFilterForOverlay(filterInput) {
// Réimplémentation simplifiée du filtre pour l'overlay
filterInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase().trim();
const rows = fullscreenOverlay.querySelectorAll('.student-row');
let visibleCount = 0;
rows.forEach(row => {
const studentName = row.dataset.studentName;
const isVisible = !searchTerm || studentName.includes(searchTerm);
row.style.display = isVisible ? '' : 'none';
if (isVisible) visibleCount++;
});
const studentCount = fullscreenOverlay.querySelector('#student-count');
if (studentCount) {
studentCount.textContent = `${visibleCount} / ${rows.length} élève(s)`;
}
});
}
// Raccourci clavier pour le plein écran (F11 ou Échap pour quitter)
document.addEventListener('keydown', function(e) {
if (e.key === 'F11') {
e.preventDefault();
toggleFullscreen();
} else if (e.key === 'Escape' && isFullscreen) {
e.preventDefault();
toggleFullscreen();
}
});
// Gestion de la fermeture de la page avec modifications non sauvegardées
window.addEventListener('beforeunload', function(e) {
if (unsavedChanges.size > 0) {
@@ -1039,4 +1351,4 @@ window.addEventListener('beforeunload', function(e) {
}
});
</script>
{% endblock %}
{% endblock %}