From c5a7a9a8a09adffa1d73e3f1607b0487b68863a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=2E?= <48770524+Arouz@users.noreply.github.com> Date: Fri, 20 Mar 2026 02:40:48 +0100 Subject: [PATCH 1/4] update front logic for data fetching --- classroom/assets/js/main/ClassroomManager.js | 37 ++++++++++++++++++- classroom/assets/js/scripts/buttons.js | 4 +- .../assets/js/scripts/dashboardActivities.js | 6 +-- classroom/assets/js/scripts/displayPanel.js | 16 +++++++- .../assets/js/scripts/manageClassroom.js | 29 ++++++--------- 5 files changed, 66 insertions(+), 26 deletions(-) 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/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..b2c7aac2 100644 --- a/classroom/assets/js/scripts/manageClassroom.js +++ b/classroom/assets/js/scripts/manageClassroom.js @@ -1994,27 +1994,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; From 840e533493990bfe62028c96d2c2ea3feaba2766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=2E?= <48770524+Arouz@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:16:45 +0100 Subject: [PATCH 2/4] update to fluid display --- classroom/assets/css/main.css | 6 + .../assets/js/scripts/classroomDisplay.js | 8 +- .../assets/js/scripts/manageClassroom.js | 173 ++++++++++++++++-- 3 files changed, 167 insertions(+), 20 deletions(-) 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/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/manageClassroom.js b/classroom/assets/js/scripts/manageClassroom.js index b2c7aac2..8a30f5cc 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 = []; // collect all row HTML strings for virtual rendering + 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,11 +1371,148 @@ function displayStudentsInClassroom(students, link=false) { } // end of the current table row html += ''; - $('#body-table-teach').append(html).localize(); - $('[data-bs-toggle="tooltip"]').tooltip() + // Embed absolute index so anonymizeStudents works correctly with virtual scroll + studentRows.push('' + html.slice(4)); }); - - appendAddStudentButton(); + + // === Virtual Scroll (table-layout: fixed, sliding window ~60 rows) === + (function vScroll(allRows, container) { + const POOL = 60; + const table = container.querySelector('table'); + const tbody = document.getElementById('body-table-teach'); + const tmpTbody = document.createElement('tbody'); // shared parser, never attached to DOM + + // Freeze column widths so table-layout:fixed doesn't squash them + Array.from(document.querySelectorAll('#header-table-teach th')).forEach(th => { + th.style.width = Math.max(th.offsetWidth, 30) + 'px'; + }); + table.style.tableLayout = 'fixed'; + + // Small class — render all at once, no virtual scroll + if (allRows.length <= POOL) { + tmpTbody.innerHTML = allRows.join(''); + Array.from(tmpTbody.children).forEach(el => tbody.appendChild(el)); + const els = Array.from(tbody.children); + $(els).find('[data-bs-toggle="tooltip"]').tooltip(); + $(els).localize(); + $(els).find('.bilan-cell').html('
'.repeat(4)); + if ($('#is-anonymised').prop('checked')) anonymizeStudents(); + appendAddStudentButton(); + return; + } + + // Spacers — only 2 extra nodes in DOM for total height + const topSp = document.createElement('tr'); + topSp.innerHTML = ''; + tbody.appendChild(topSp); + + // Build initial pool (rows 0..POOL-1) + tmpTbody.innerHTML = allRows.slice(0, POOL).join(''); + const pool = Array.from(tmpTbody.children); // pool[i] is always in DOM order + pool.forEach(tr => tbody.appendChild(tr)); + + // postProcess all pool rows once at init + $(pool).find('[data-bs-toggle="tooltip"]').tooltip(); + $(pool).localize(); + $(pool).find('.bilan-cell').html('
'.repeat(4)); + if ($('#is-anonymised').prop('checked')) anonymizeStudents(); + + const botSp = document.createElement('tr'); + botSp.innerHTML = ''; + tbody.appendChild(botSp); + appendAddStudentButton(); // permanently after botSp, never touched by scroll + + const rowH = (pool[0] && pool[0].offsetHeight) || 44; + let anchor = 0; // pool[0] shows allRows[anchor], pool[POOL-1] shows allRows[anchor+POOL-1] + + function setSpacers(a) { + topSp.firstElementChild.style.height = Math.max(0, a * rowH) + 'px'; + botSp.firstElementChild.style.height = Math.max(0, (allRows.length - a - POOL) * rowH) + 'px'; + } + setSpacers(0); + + // Update an existing node with the content of allRows[rowIdx] + function fillRow(tr, rowIdx) { + // No dispose needed: tr.innerHTML detaches old children from DOM, + // making their Bootstrap event listeners permanently inactive. + tmpTbody.innerHTML = allRows[rowIdx]; + const src = tmpTbody.firstElementChild; + // Sync attributes + while (tr.attributes.length) tr.removeAttribute(tr.attributes[0].name); + for (const attr of src.attributes) tr.setAttribute(attr.name, attr.value); + tr.innerHTML = src.innerHTML; + // Cheap immediate ops (no DOM structural change) + $(tr).localize(); + $(tr).find('.bilan-cell').html('
'.repeat(4)); + } + + // Expensive tooltip init deferred until scroll stops + let ppTimer = null; + function scheduleTooltips() { + clearTimeout(ppTimer); + ppTimer = setTimeout(() => { + $(pool).find('[data-bs-toggle="tooltip"]').tooltip(); + if ($('#is-anonymised').prop('checked')) anonymizeStudents(); + }, 200); + } + + // Scroll down by delta: recycle front rows to back + function recycleDown(delta) { + for (let i = 0; i < delta; i++) { + const tr = pool.shift(); + fillRow(tr, anchor + POOL + i); + tbody.insertBefore(tr, botSp); // DOM move: old front → new back + pool.push(tr); + } + anchor += delta; + setSpacers(anchor); + } + + // Scroll up by delta: recycle back rows to front (iterate reverse for correct DOM order) + function recycleUp(delta) { + const ref = pool[0]; // fixed anchor point; all insertions go before this + const newEntries = []; + for (let i = delta - 1; i >= 0; i--) { + const tr = pool.pop(); + fillRow(tr, anchor - delta + i); + tbody.insertBefore(tr, ref); // reverse iteration → ascending visual order + newEntries.unshift(tr); + } + pool.unshift(...newEntries); + anchor -= delta; + setSpacers(anchor); + } + + let raf = null; + function update() { + const sT = container.scrollTop; + const visFirst = Math.floor(sT / rowH); + // Keep a small above-fold buffer (3 rows) to cover fast upward scrolls + const newAnchor = Math.max(0, Math.min(Math.max(0, visFirst - 3), allRows.length - POOL)); + if (newAnchor === anchor) return; + + const delta = newAnchor - anchor; + if (Math.abs(delta) >= POOL) { + // Large jump: rewrite all pool slots, no DOM moves needed + anchor = newAnchor; + pool.forEach((tr, i) => fillRow(tr, newAnchor + i)); + setSpacers(anchor); + } else if (delta > 0) { + recycleDown(delta); + } else { + recycleUp(-delta); + } + scheduleTooltips(); + } + + function onScroll() { + if (raf) return; + raf = requestAnimationFrame(() => { raf = null; update(); }); + } + container.addEventListener('scroll', onScroll, { passive: true }); + + })(studentRows, document.getElementById('classroom-panel-table-container')); + // === End Virtual Scroll === // get classroom settings from localstorage let settings = getClassroomDisplaySettings(link); @@ -1418,18 +1556,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') { From 8952199f6fd94c137d30e57be9c1702337161526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=2E?= <48770524+Arouz@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:51:10 +0100 Subject: [PATCH 3/4] inline comentary deletion --- .../assets/js/scripts/manageClassroom.js | 69 ++++++++++++++----- .../assets/js/scripts/teacherActivities.js | 15 ++-- composer.json | 4 +- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/classroom/assets/js/scripts/manageClassroom.js b/classroom/assets/js/scripts/manageClassroom.js index 8a30f5cc..f3426f3c 100644 --- a/classroom/assets/js/scripts/manageClassroom.js +++ b/classroom/assets/js/scripts/manageClassroom.js @@ -1080,7 +1080,7 @@ function displayStudentsInClassroom(students, link=false) { // get the current classroom index of activities let arrayIndexesActivities = listIndexesActivities(students); - const studentRows = []; // collect all row HTML strings for virtual rendering + 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); @@ -1380,7 +1380,11 @@ function displayStudentsInClassroom(students, link=false) { const POOL = 60; const table = container.querySelector('table'); const tbody = document.getElementById('body-table-teach'); - const tmpTbody = document.createElement('tbody'); // shared parser, never attached to DOM + const tmpTbody = document.createElement('tbody'); + + // Neutralize img src in all HTML strings so tr.innerHTML never triggers a network request. + // fillRow will move the existing pooled nodes (already loaded) instead. + const allRowsSafe = allRows.map(html => html.replace(/(]*?)\ssrc=/gi, '$1 data-vs-src=')); // Freeze column widths so table-layout:fixed doesn't squash them Array.from(document.querySelectorAll('#header-table-teach th')).forEach(th => { @@ -1408,7 +1412,7 @@ function displayStudentsInClassroom(students, link=false) { // Build initial pool (rows 0..POOL-1) tmpTbody.innerHTML = allRows.slice(0, POOL).join(''); - const pool = Array.from(tmpTbody.children); // pool[i] is always in DOM order + const pool = Array.from(tmpTbody.children); pool.forEach(tr => tbody.appendChild(tr)); // postProcess all pool rows once at init @@ -1420,10 +1424,10 @@ function displayStudentsInClassroom(students, link=false) { const botSp = document.createElement('tr'); botSp.innerHTML = ''; tbody.appendChild(botSp); - appendAddStudentButton(); // permanently after botSp, never touched by scroll + appendAddStudentButton(); const rowH = (pool[0] && pool[0].offsetHeight) || 44; - let anchor = 0; // pool[0] shows allRows[anchor], pool[POOL-1] shows allRows[anchor+POOL-1] + let anchor = 0; function setSpacers(a) { topSp.firstElementChild.style.height = Math.max(0, a * rowH) + 'px'; @@ -1431,17 +1435,48 @@ function displayStudentsInClassroom(students, link=false) { } setSpacers(0); - // Update an existing node with the content of allRows[rowIdx] + // Reusable anchor element to resolve any relative URL → absolute, once. + const _urlResolver = document.createElement('a'); + function resolveUrl(url) { _urlResolver.href = url; return _urlResolver.href; } + + // Update an existing node with the content of allRows[rowIdx]. + // Uses allRowsSafe (no img src) so tr.innerHTML never fires a network request. + // Existing pooled nodes are moved via replaceWith() — zero requests. function fillRow(tr, rowIdx) { - // No dispose needed: tr.innerHTML detaches old children from DOM, - // making their Bootstrap event listeners permanently inactive. - tmpTbody.innerHTML = allRows[rowIdx]; - const src = tmpTbody.firstElementChild; - // Sync attributes + // Parse the safe version (no img src) into tmpTbody + tmpTbody.innerHTML = allRowsSafe[rowIdx]; + const newRow = tmpTbody.firstElementChild; + + // Capture existing img nodes and their target src from data-vs-src + // Resolve to absolute URL so comparison with img.src (always absolute) works correctly + const existingImgs = Array.from(tr.querySelectorAll('img')); + const newImgMeta = Array.from(newRow.querySelectorAll('img')).map(img => ({ + src: img.dataset.vsSrc ? resolveUrl(img.dataset.vsSrc) : '', + alt: img.alt, + className: img.className + })); + + // Sync tr attributes (preserves data-student-idx etc.) while (tr.attributes.length) tr.removeAttribute(tr.attributes[0].name); - for (const attr of src.attributes) tr.setAttribute(attr.name, attr.value); - tr.innerHTML = src.innerHTML; - // Cheap immediate ops (no DOM structural change) + for (const attr of newRow.attributes) tr.setAttribute(attr.name, attr.value); + tr.innerHTML = newRow.innerHTML; + + // Re-insert existing img nodes (DOM move = guaranteed zero request) + // Only update .src if the letter actually changed (pool of 26 images) + const freshImgs = Array.from(tr.querySelectorAll('img')); + freshImgs.forEach((freshImg, i) => { + const saved = existingImgs[i]; + if (!saved) return; + const meta = newImgMeta[i]; + if (meta) { + if (meta.src && saved.src !== meta.src) saved.src = meta.src; + saved.alt = meta.alt; + saved.className = meta.className; + } + freshImg.replaceWith(saved); + }); + + // Cheap immediate ops $(tr).localize(); $(tr).find('.bilan-cell').html('
'.repeat(4)); } @@ -1461,7 +1496,7 @@ function displayStudentsInClassroom(students, link=false) { for (let i = 0; i < delta; i++) { const tr = pool.shift(); fillRow(tr, anchor + POOL + i); - tbody.insertBefore(tr, botSp); // DOM move: old front → new back + tbody.insertBefore(tr, botSp); pool.push(tr); } anchor += delta; @@ -1470,12 +1505,12 @@ function displayStudentsInClassroom(students, link=false) { // Scroll up by delta: recycle back rows to front (iterate reverse for correct DOM order) function recycleUp(delta) { - const ref = pool[0]; // fixed anchor point; all insertions go before this + const ref = pool[0]; const newEntries = []; for (let i = delta - 1; i >= 0; i--) { const tr = pool.pop(); fillRow(tr, anchor - delta + i); - tbody.insertBefore(tr, ref); // reverse iteration → ascending visual order + tbody.insertBefore(tr, ref); newEntries.unshift(tr); } pool.unshift(...newEntries); 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" From 05c4524bde8317bb7f5119f0e1131eb75856a211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=2E?= <48770524+Arouz@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:19:22 +0100 Subject: [PATCH 4/4] update display for dashboard --- .../assets/js/scripts/manageClassroom.js | 188 ++---------------- 1 file changed, 14 insertions(+), 174 deletions(-) diff --git a/classroom/assets/js/scripts/manageClassroom.js b/classroom/assets/js/scripts/manageClassroom.js index f3426f3c..f9c0a0b1 100644 --- a/classroom/assets/js/scripts/manageClassroom.js +++ b/classroom/assets/js/scripts/manageClassroom.js @@ -1371,183 +1371,23 @@ function displayStudentsInClassroom(students, link=false) { } // end of the current table row html += ''; - // Embed absolute index so anonymizeStudents works correctly with virtual scroll - studentRows.push('' + html.slice(4)); + studentRows.push(html); }); - // === Virtual Scroll (table-layout: fixed, sliding window ~60 rows) === - (function vScroll(allRows, container) { - const POOL = 60; - const table = container.querySelector('table'); - const tbody = document.getElementById('body-table-teach'); - const tmpTbody = document.createElement('tbody'); - - // Neutralize img src in all HTML strings so tr.innerHTML never triggers a network request. - // fillRow will move the existing pooled nodes (already loaded) instead. - const allRowsSafe = allRows.map(html => html.replace(/(]*?)\ssrc=/gi, '$1 data-vs-src=')); - - // Freeze column widths so table-layout:fixed doesn't squash them - Array.from(document.querySelectorAll('#header-table-teach th')).forEach(th => { - th.style.width = Math.max(th.offsetWidth, 30) + 'px'; - }); - table.style.tableLayout = 'fixed'; - - // Small class — render all at once, no virtual scroll - if (allRows.length <= POOL) { - tmpTbody.innerHTML = allRows.join(''); - Array.from(tmpTbody.children).forEach(el => tbody.appendChild(el)); - const els = Array.from(tbody.children); - $(els).find('[data-bs-toggle="tooltip"]').tooltip(); - $(els).localize(); - $(els).find('.bilan-cell').html('
'.repeat(4)); - if ($('#is-anonymised').prop('checked')) anonymizeStudents(); - appendAddStudentButton(); - return; - } - - // Spacers — only 2 extra nodes in DOM for total height - const topSp = document.createElement('tr'); - topSp.innerHTML = ''; - tbody.appendChild(topSp); - - // Build initial pool (rows 0..POOL-1) - tmpTbody.innerHTML = allRows.slice(0, POOL).join(''); - const pool = Array.from(tmpTbody.children); - pool.forEach(tr => tbody.appendChild(tr)); - - // postProcess all pool rows once at init - $(pool).find('[data-bs-toggle="tooltip"]').tooltip(); - $(pool).localize(); - $(pool).find('.bilan-cell').html('
'.repeat(4)); - if ($('#is-anonymised').prop('checked')) anonymizeStudents(); - - const botSp = document.createElement('tr'); - botSp.innerHTML = ''; - tbody.appendChild(botSp); - appendAddStudentButton(); - - const rowH = (pool[0] && pool[0].offsetHeight) || 44; - let anchor = 0; - - function setSpacers(a) { - topSp.firstElementChild.style.height = Math.max(0, a * rowH) + 'px'; - botSp.firstElementChild.style.height = Math.max(0, (allRows.length - a - POOL) * rowH) + 'px'; - } - setSpacers(0); - - // Reusable anchor element to resolve any relative URL → absolute, once. - const _urlResolver = document.createElement('a'); - function resolveUrl(url) { _urlResolver.href = url; return _urlResolver.href; } - - // Update an existing node with the content of allRows[rowIdx]. - // Uses allRowsSafe (no img src) so tr.innerHTML never fires a network request. - // Existing pooled nodes are moved via replaceWith() — zero requests. - function fillRow(tr, rowIdx) { - // Parse the safe version (no img src) into tmpTbody - tmpTbody.innerHTML = allRowsSafe[rowIdx]; - const newRow = tmpTbody.firstElementChild; - - // Capture existing img nodes and their target src from data-vs-src - // Resolve to absolute URL so comparison with img.src (always absolute) works correctly - const existingImgs = Array.from(tr.querySelectorAll('img')); - const newImgMeta = Array.from(newRow.querySelectorAll('img')).map(img => ({ - src: img.dataset.vsSrc ? resolveUrl(img.dataset.vsSrc) : '', - alt: img.alt, - className: img.className - })); - - // Sync tr attributes (preserves data-student-idx etc.) - while (tr.attributes.length) tr.removeAttribute(tr.attributes[0].name); - for (const attr of newRow.attributes) tr.setAttribute(attr.name, attr.value); - tr.innerHTML = newRow.innerHTML; - - // Re-insert existing img nodes (DOM move = guaranteed zero request) - // Only update .src if the letter actually changed (pool of 26 images) - const freshImgs = Array.from(tr.querySelectorAll('img')); - freshImgs.forEach((freshImg, i) => { - const saved = existingImgs[i]; - if (!saved) return; - const meta = newImgMeta[i]; - if (meta) { - if (meta.src && saved.src !== meta.src) saved.src = meta.src; - saved.alt = meta.alt; - saved.className = meta.className; - } - freshImg.replaceWith(saved); - }); - - // Cheap immediate ops - $(tr).localize(); - $(tr).find('.bilan-cell').html('
'.repeat(4)); - } - - // Expensive tooltip init deferred until scroll stops - let ppTimer = null; - function scheduleTooltips() { - clearTimeout(ppTimer); - ppTimer = setTimeout(() => { - $(pool).find('[data-bs-toggle="tooltip"]').tooltip(); - if ($('#is-anonymised').prop('checked')) anonymizeStudents(); - }, 200); - } - - // Scroll down by delta: recycle front rows to back - function recycleDown(delta) { - for (let i = 0; i < delta; i++) { - const tr = pool.shift(); - fillRow(tr, anchor + POOL + i); - tbody.insertBefore(tr, botSp); - pool.push(tr); - } - anchor += delta; - setSpacers(anchor); - } - - // Scroll up by delta: recycle back rows to front (iterate reverse for correct DOM order) - function recycleUp(delta) { - const ref = pool[0]; - const newEntries = []; - for (let i = delta - 1; i >= 0; i--) { - const tr = pool.pop(); - fillRow(tr, anchor - delta + i); - tbody.insertBefore(tr, ref); - newEntries.unshift(tr); - } - pool.unshift(...newEntries); - anchor -= delta; - setSpacers(anchor); - } - - let raf = null; - function update() { - const sT = container.scrollTop; - const visFirst = Math.floor(sT / rowH); - // Keep a small above-fold buffer (3 rows) to cover fast upward scrolls - const newAnchor = Math.max(0, Math.min(Math.max(0, visFirst - 3), allRows.length - POOL)); - if (newAnchor === anchor) return; - - const delta = newAnchor - anchor; - if (Math.abs(delta) >= POOL) { - // Large jump: rewrite all pool slots, no DOM moves needed - anchor = newAnchor; - pool.forEach((tr, i) => fillRow(tr, newAnchor + i)); - setSpacers(anchor); - } else if (delta > 0) { - recycleDown(delta); - } else { - recycleUp(-delta); - } - scheduleTooltips(); - } - - function onScroll() { - if (raf) return; - raf = requestAnimationFrame(() => { raf = null; update(); }); - } - container.addEventListener('scroll', onScroll, { passive: true }); + // 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`; + }); - })(studentRows, document.getElementById('classroom-panel-table-container')); - // === End Virtual Scroll === + // 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);