diff --git a/classroom/assets/css/main.css b/classroom/assets/css/main.css index ddc97aee..9fea75de 100644 --- a/classroom/assets/css/main.css +++ b/classroom/assets/css/main.css @@ -937,6 +937,12 @@ button { +/* Skip rendering of off-screen rows — browser handles it natively, no JS needed */ +#body-table-teach tr { + content-visibility: auto; + contain-intrinsic-size: 0 44px; +} + #body-table-teach .propic { width: 36px; height: 36px; diff --git a/classroom/assets/js/main/ClassroomManager.js b/classroom/assets/js/main/ClassroomManager.js index 9525db8a..b260241b 100644 --- a/classroom/assets/js/main/ClassroomManager.js +++ b/classroom/assets/js/main/ClassroomManager.js @@ -286,6 +286,14 @@ class ClassroomManager { classroom.classroom.name = `${classroom.classroom.name} | ${classroom.classroom.groupe}`; } } + // Preserve already lazy-loaded students so they are not wiped + const oldClasses = container._myClasses || []; + for (let newEntry of response) { + const existing = oldClasses.find(c => c.classroom.id === newEntry.classroom.id); + if (existing && existing.students.length > 0) { + newEntry.students = existing.students; + } + } container._myClasses = response; Main.getClassroomManager().countClassroomAndStudents(response); onEnd(); @@ -310,11 +318,38 @@ class ClassroomManager { }; data.forEach((classroom) => { teacherData.classrooms++; - teacherData.students += classroom.students.length; + // Use server-provided studentCount (students array is lazy-loaded) + teacherData.students += classroom.studentCount ?? classroom.students.length; }); UserManager.getUser().teacherData = teacherData; } + /** + * Load full student data for one classroom (lazy, called on click). + * Stores result in _myClasses so subsequent clicks don't re-fetch. + */ + getClassroomStudents(classroomId) { + return new Promise((resolve, reject) => { + $.ajax({ + type: 'POST', + dataType: 'JSON', + url: '/routing/Routing.php?controller=classroom&action=get_classroom_students', + data: { classroomId: classroomId }, + success: (response) => { + if (response.error) { + console.error('getClassroomStudents error:', response.error); + resolve([]); + return; + } + const entry = this._myClasses.find(c => c.classroom.id == classroomId); + if (entry) entry.students = response.students; + resolve(response.students); + }, + error: (e) => { console.error(e); reject(e); } + }); + }); + } + /** * Get teachers from the classroom * Access with Main.getClassroomManager()._myTeachers diff --git a/classroom/assets/js/scripts/buttons.js b/classroom/assets/js/scripts/buttons.js index 8af9555c..59c1cee5 100644 --- a/classroom/assets/js/scripts/buttons.js +++ b/classroom/assets/js/scripts/buttons.js @@ -1077,7 +1077,7 @@ function classroomsDisplay() { let classes = Main.getClassroomManager()._myClasses; if (classes.length) { classes.forEach(classroom => { - $('.list-classes').append(classeItem(classroom.classroom, classroom.students.length, classroom.students)); + $('.list-classes').append(classeItem(classroom.classroom, classroom.studentCount ?? classroom.students.length, classroom.students, classroom.pendingCorrections, classroom.activitiesCount)); }); } else { $('.list-classes').append(noContentDiv).localize(); @@ -1088,7 +1088,7 @@ function classroomsDisplay() { let classes = Main.getClassroomManager()._myClasses; if (classes.length) { classes.forEach(classroom => { - $('.list-classes').append(classeItem(classroom.classroom, classroom.students.length, classroom.students)); + $('.list-classes').append(classeItem(classroom.classroom, classroom.studentCount ?? classroom.students.length, classroom.students, classroom.pendingCorrections, classroom.activitiesCount)); }); } else { $('.list-classes').append(noContentDiv).localize(); diff --git a/classroom/assets/js/scripts/classroomDisplay.js b/classroom/assets/js/scripts/classroomDisplay.js index 3275ffcd..aecfb830 100644 --- a/classroom/assets/js/scripts/classroomDisplay.js +++ b/classroom/assets/js/scripts/classroomDisplay.js @@ -78,11 +78,13 @@ $('body').on('change', '#is-anonymised', function () { }); function anonymizeStudents() { - $('.username').each(function (index,el) { + $('.username').each(function (index, el) { + // Use absolute student index embedded during virtual scroll rendering + const absIdx = parseInt($(el).closest('tr').attr('data-student-idx') ?? index); $(el).children().children('img').attr('src', _PATH + 'assets/media/alphabet/E.png') - $(el).children().children('img').attr('alt', 'Photo de profil anonymisée - Étudiant ' + (index + 1)) + $(el).children().children('img').attr('alt', 'Photo de profil anonymisée - Étudiant ' + (absIdx + 1)) $(el).children().children('img').attr('anonymized', 'true') - $(el).children().children('.user-cell-username').text(i18next.t('classroom.activities.anoStudent') + " " + index) + $(el).children().children('.user-cell-username').text(i18next.t('classroom.activities.anoStudent') + " " + absIdx) $(el).children().children('.user-cell-username').attr('title', '') }) } diff --git a/classroom/assets/js/scripts/dashboardActivities.js b/classroom/assets/js/scripts/dashboardActivities.js index eb361165..08faa901 100644 --- a/classroom/assets/js/scripts/dashboardActivities.js +++ b/classroom/assets/js/scripts/dashboardActivities.js @@ -342,7 +342,7 @@ function teacherFolder(folder, displayStyle) { } -function classeItem(classe, nbStudents, students) { +function classeItem(classe, nbStudents, students, overridePendingCorrections = null, overrideActivitiesCount = null) { function maxLength(array) { let count = 0 for (let i = 0; i < array.length; i++) { @@ -353,8 +353,8 @@ function classeItem(classe, nbStudents, students) { return count } - let maxAct = maxLength(students) - let remainingCorrections = getRemainingCorrections(students); + let maxAct = overrideActivitiesCount !== null ? overrideActivitiesCount : maxLength(students); + let remainingCorrections = overridePendingCorrections !== null ? overridePendingCorrections : getRemainingCorrections(students); let remainingCorrectionsSpanElt = remainingCorrections ? ` ${remainingCorrections}` : ''; let html = `
diff --git a/classroom/assets/js/scripts/displayPanel.js b/classroom/assets/js/scripts/displayPanel.js index 3c4ae2d6..331784c3 100644 --- a/classroom/assets/js/scripts/displayPanel.js +++ b/classroom/assets/js/scripts/displayPanel.js @@ -27,7 +27,10 @@ DisplayPanel.prototype.classroom_dashboard_profil_panel_teacher = function () { }) getIntelFromClasses(); - const correctionsCount = getRemainingCorrections(Main.getClassroomManager()._myClasses.flatMap(c => c.students)); + // pendingCorrections is pre-computed server-side in the lightweight get_by_user response + const correctionsCount = Main.getClassroomManager()._myClasses.reduce( + (sum, c) => sum + (c.pendingCorrections ?? getRemainingCorrections(c.students ?? [])), 0 + ); const correctionsElement = $('.tocorrect-activities'); correctionsElement.html(correctionsCount); @@ -468,7 +471,16 @@ function updateUIWithStudents(link) { updateClassroomLinkUI(ClassroomSettings.classroom); updateClassroomLockUI(classroomData.classroom.isBlocked); - displayStudentsInClassroom(classroomData.students, link); + + if (classroomData.students && classroomData.students.length > 0) { + // Already loaded (cached from a previous click) + displayStudentsInClassroom(classroomData.students, link); + } else { + // Lazy-load: first click on this classroom + Main.getClassroomManager().getClassroomStudents(classroomData.classroom.id).then(students => { + displayStudentsInClassroom(students, link); + }); + } } DisplayPanel.prototype.classroom_dashboard_new_activity_panel3 = function (ref) { diff --git a/classroom/assets/js/scripts/manageClassroom.js b/classroom/assets/js/scripts/manageClassroom.js index 50b723ed..f9c0a0b1 100644 --- a/classroom/assets/js/scripts/manageClassroom.js +++ b/classroom/assets/js/scripts/manageClassroom.js @@ -1080,7 +1080,8 @@ function displayStudentsInClassroom(students, link=false) { // get the current classroom index of activities let arrayIndexesActivities = listIndexesActivities(students); - students.forEach(element => { + const studentRows = []; + students.forEach((element, elementIdx) => { // reorder the current student activities to fit to the classroom index of activities let arrayActivities = reorderActivities([...element.activities, ...element.courses], arrayIndexesActivities); let pseudo = element.user.pseudo; @@ -1370,10 +1371,22 @@ function displayStudentsInClassroom(students, link=false) { } // end of the current table row html += ''; - $('#body-table-teach').append(html).localize(); - $('[data-bs-toggle="tooltip"]').tooltip() + studentRows.push(html); }); - + + // Preload unique letter images (max 26 requests); browser cache serves subsequent rows + new Set(students.map(s => getFirstLetterOfPseudo(s.user.pseudo))).forEach(letter => { + const _img = new Image(); + _img.src = `${_PATH}assets/media/alphabet/${letter}.png`; + }); + + // Render all students directly into the table + const tbody = document.getElementById('body-table-teach'); + const tmpTbody = document.createElement('tbody'); + tmpTbody.innerHTML = studentRows.join(''); + Array.from(tmpTbody.children).forEach(el => tbody.appendChild(el)); + $(tbody).find('[data-bs-toggle="tooltip"]').tooltip(); + $(tbody).localize(); appendAddStudentButton(); // get classroom settings from localstorage let settings = getClassroomDisplaySettings(link); @@ -1418,18 +1431,19 @@ function displayStudentsInClassroom(students, link=false) { // add four empty divs for monochrome styling $('#body-table-teach .bilan-cell').html(`
`); - $('#classroom-panel-table-container table .dropdown').on('show.bs.dropdown', (event) => { - let classroomTable = event.target.closest('table'); - classroomTable.classList.add('dropdowns-opened'); - $(classroomTable).find('tr').addClass('non-dropdown'); - event.target.closest('tr').classList.remove('non-dropdown'); - }); - - $('#classroom-panel-table-container table .dropdown').on('hidden.bs.dropdown', (event) => { - let classroomTable = event.target.closest('table'); - classroomTable.classList.remove('dropdowns-opened'); - $(classroomTable).find('tr').removeClass('non-dropdown'); - }); + // Delegated event binding so lazy-loaded rows also receive these handlers + $('#classroom-panel-table-container table').off('show.bs.dropdown.lazyrows hidden.bs.dropdown.lazyrows') + .on('show.bs.dropdown.lazyrows', '.dropdown', (event) => { + let classroomTable = event.target.closest('table'); + classroomTable.classList.add('dropdowns-opened'); + $(classroomTable).find('tr').addClass('non-dropdown'); + event.target.closest('tr').classList.remove('non-dropdown'); + }) + .on('hidden.bs.dropdown.lazyrows', '.dropdown', (event) => { + let classroomTable = event.target.closest('table'); + classroomTable.classList.remove('dropdowns-opened'); + $(classroomTable).find('tr').removeClass('non-dropdown'); + }); // Plugin hook: post-render callback (no-op without plugin) if (typeof window.onDashboardRendered === 'function') { @@ -1994,27 +2008,20 @@ class DashboardAutoRefresh { refresh() { if($_GET('panel') == 'classroom-table-panel-teacher' && $_GET('option')){ this.isRefreshing = true; - let previousClassroomState, newClassroomState; - if (getClassroomInListByLink($_GET('option'))[0]) { - previousClassroomState = { - data: JSON.stringify(getClassroomInListByLink($_GET('option'))[0].students), - link: $_GET('option') - }; - } - Main.getClassroomManager().getClasses(Main.getClassroomManager()).then(() => { - if ($_GET('option') == previousClassroomState.link) { - if (getClassroomInListByLink($_GET('option'))[0]) { - newClassroomState = JSON.stringify(getClassroomInListByLink($_GET('option'))[0].students); - } - // Only refresh the classroom if it has changed - if (previousClassroomState.data != newClassroomState){ - if (getClassroomInListByLink($_GET('option'))[0]) { - let students = getClassroomInListByLink($_GET('option'))[0].students; + const link = $_GET('option'); + const entry = getClassroomInListByLink(link)?.[0]; + if (entry) { + const previousStudentsJson = JSON.stringify(entry.students); + Main.getClassroomManager().getClassroomStudents(entry.classroom.id).then((students) => { + if ($_GET('option') == link) { + const newStudentsJson = JSON.stringify(students); + // Only refresh the classroom display if data has changed + if (previousStudentsJson != newStudentsJson) { displayStudentsInClassroom(students); } } - } - }); + }); + } setTimeout(() => { this.refresh() }, this.refreshInterval); } else { this.isRefreshing = false; diff --git a/classroom/assets/js/scripts/teacherActivities.js b/classroom/assets/js/scripts/teacherActivities.js index 2c6e0a51..5105d48d 100644 --- a/classroom/assets/js/scripts/teacherActivities.js +++ b/classroom/assets/js/scripts/teacherActivities.js @@ -380,13 +380,18 @@ function listStudentsToAttribute(ref = null) { if (classes.length == 0) { $('#attribute-activity-to-students-close').after(NO_CLASS) $('#attribute-activity-to-students-close').hide() - } else { - classes.forEach(element => { - $('#list-student-attribute-modal').append(classeList(element, ref)) + const pending = classes + .filter(c => c.students.length === 0) + .map(c => Main.getClassroomManager().getClassroomStudents(c.classroom.id)); + + Promise.all(pending).then(() => { + classes.forEach(element => { + $('#list-student-attribute-modal').append(classeList(element, ref)) + }); + $('.no-classes').remove() + $('#attribute-activity-to-students-close').show() }); - $('.no-classes').remove() - $('#attribute-activity-to-students-close').show() } } diff --git a/composer.json b/composer.json index 38f83c1b..3e17f1da 100644 --- a/composer.json +++ b/composer.json @@ -12,9 +12,9 @@ }, "require-dev": { "phpunit/phpunit": "8.0.0", - "vittascience/vtuser": "1.3.5", + "vittascience/vtuser": "1.3.6", "vittascience/vtinterfaces": "1.5.8", - "vittascience/vtclassroom": "1.4.3", + "vittascience/vtclassroom": "dev-arz-optimisation", "vittascience/vtlearn": "1.4.6", "vittascience/vutils": "1.3.1", "symfony/var-dumper": "^5.3"