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);