Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions accessibility.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// accessibility.js - Core utilities for quiz accessibility and keyboard navigation
// ------------------------------------------------------------
// This script provides functions to initialize ARIA roles, manage focus, and handle
// keyboard shortcuts for quiz pages. It attaches its API to the global `window` object.

(function () {
// Initialize accessibility for a quiz.
function initQuizAccessibility({ containerId = 'quiz-box', optionsContainerId = 'options', statusId = 'sr-status' } = {}) {
const container = document.getElementById(containerId);
const optionsContainer = document.getElementById(optionsContainerId);
const srStatus = document.getElementById(statusId);

// Ensure the options container has proper ARIA role (radiogroup) if not already set.
if (optionsContainer && !optionsContainer.getAttribute('role')) {
optionsContainer.setAttribute('role', 'radiogroup');
}

// Attach a keydown listener for navigation shortcuts.
document.addEventListener('keydown', (e) => handleKeyNavigation(e, optionsContainer, srStatus));

return { container, optionsContainer, srStatus };
}

// Keyboard navigation handler.
function handleKeyNavigation(event, optionsContainer, srStatus) {
const KEY = {
LEFT: 'ArrowLeft',
RIGHT: 'ArrowRight',
ENTER: 'Enter',
SPACE: ' ',
CTRL_ENTER: 'Enter',
};

const activeEl = document.activeElement;
const isOption = activeEl && activeEl.classList && activeEl.classList.contains('option');

if (event.key === KEY.LEFT && isOption) {
focusPrevOption(activeEl);
event.preventDefault();
return;
}
if (event.key === KEY.RIGHT && isOption) {
focusNextOption(activeEl);
event.preventDefault();
return;
}
if ((event.key === KEY.ENTER || event.key === KEY.SPACE) && isOption) {
activeEl.click();
event.preventDefault();
return;
}
// Ctrl+Enter to submit current answer (if submit button is visible).
if (event.key === KEY.CTRL_ENTER && event.ctrlKey) {
const submitBtn = document.getElementById('submit-btn');
if (submitBtn && !submitBtn.classList.contains('hidden')) {
submitBtn.click();
event.preventDefault();
}
return;
}
}

function focusPrevOption(current) {
const options = Array.from(document.querySelectorAll('.option'));
const idx = options.indexOf(current);
if (idx > 0) {
options[idx - 1].focus();
announceOptionChange(options[idx - 1]);
}
}

function focusNextOption(current) {
const options = Array.from(document.querySelectorAll('.option'));
const idx = options.indexOf(current);
if (idx < options.length - 1) {
options[idx + 1].focus();
announceOptionChange(options[idx + 1]);
}
}

function announceOptionChange(optionEl) {
const sr = document.getElementById('sr-status');
if (sr) {
sr.textContent = `Option ${optionEl.textContent.trim()} selected`;
}
}

function announce(message) {
const sr = document.getElementById('sr-status');
if (sr) {
sr.textContent = '';
setTimeout(() => {
sr.textContent = message;
}, 0);
}
}

// Detect reduced motion preference and add a class to the body.
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.body.classList.add('prefers-reduced-motion');
}
// Detect high‑contrast preference (CSS Level 4 media query) and add a class.
if (window.matchMedia('(prefers-contrast: more)').matches) {
document.body.classList.add('high-contrast');
}

// Expose API globally.
window.initQuizAccessibility = initQuizAccessibility;
window.srAnnounce = announce;
})();
85 changes: 85 additions & 0 deletions class_manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// class_manager.js – Handles teacher class sessions and progress aggregation

const CLASS_SESSIONS_KEY = 'learnsphere_class_sessions_v1';

function _loadClasses() {
try {
const raw = localStorage.getItem(CLASS_SESSIONS_KEY);
return raw ? JSON.parse(raw) : [];
} catch (e) {
console.warn('LearnSphere: Failed to load class sessions', e);
return [];
}
}

function _saveClasses(classes) {
try {
localStorage.setItem(CLASS_SESSIONS_KEY, JSON.stringify(classes));
} catch (e) {
console.warn('LearnSphere: Failed to save class sessions', e);
}
}

function generateInviteCode(length = 6) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let code = '';
for (let i = 0; i < length; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
return code;
}

function createClassSession({ name, topics = [], dueDate }) {
const classes = _loadClasses();
const id = 'cls_' + Date.now();
const inviteCode = generateInviteCode();
const newClass = {
id,
name,
inviteCode,
topics,
dueDate: dueDate || null,
attempts: [], // each { studentId, quizId, score }
createdAt: new Date().toISOString()
};
classes.push(newClass);
_saveClasses(classes);
return newClass;
}

function recordClassAttempt(classId, { studentId = 'anonymous', quizId, score }) {
if (!classId) return;
const classes = _loadClasses();
const cls = classes.find(c => c.id === classId);
if (!cls) return;
cls.attempts.push({ studentId, quizId, score, timestamp: Date.now() });
_saveClasses(classes);
}

function getClassStats(classId) {
const classes = _loadClasses();
const cls = classes.find(c => c.id === classId);
if (!cls) return null;
const total = cls.attempts.length;
const avgScore = total ? (cls.attempts.reduce((a, b) => a + b.score, 0) / total).toFixed(2) : '-';
const bestScore = total ? Math.max(...cls.attempts.map(a => a.score)).toFixed(2) : '-';
return { ...cls, totalAttempts: total, avgScore, bestScore };
}

function getAllClassStats() {
const classes = _loadClasses();
return classes.map(c => {
const total = c.attempts.length;
const avgScore = total ? (c.attempts.reduce((a, b) => a + b.score, 0) / total).toFixed(2) : '-';
const bestScore = total ? Math.max(...c.attempts.map(a => a.score)).toFixed(2) : '-';
return { id: c.id, name: c.name, inviteCode: c.inviteCode, totalAttempts: total, avgScore, bestScore };
});
}

// Expose globally for other modules
window.classManager = {
createClassSession,
recordClassAttempt,
getClassStats,
getAllClassStats
};
72 changes: 70 additions & 2 deletions exportProgress.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,12 +374,80 @@
a.click();
a.remove();


setTimeout(() => URL.revokeObjectURL(url), 5000);
}

// Snapshot utilities
async function generateSnapshot() {
const payload = buildProgressExportPayload({ roleContext: "learner" });
const exportId = `snapshot_${Date.now()}_${Math.random().toString(16).slice(2)}`;
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(payload));
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const payloadHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
const snapshot = { payload, payloadHash, generatedAt: new Date().toISOString() };
localStorage.setItem(`snapshot_${exportId}`, JSON.stringify(snapshot));
const index = JSON.parse(localStorage.getItem('snapshotIndex') || '[]');
index.push({ exportId, generatedAt: snapshot.generatedAt, payloadHash });
localStorage.setItem('snapshotIndex', JSON.stringify(index));
return exportId;
}

function downloadSnapshotAsImage(exportId) {
const snapshotStr = localStorage.getItem(`snapshot_${exportId}`);
if (!snapshotStr) { alert('Snapshot not found'); return; }
const snapshot = JSON.parse(snapshotStr);
const html = `<html><body><pre>${JSON.stringify(snapshot.payload, null, 2)}</pre></body></html>`;
const iframe = document.createElement('iframe');
iframe.style.position = 'fixed'; iframe.style.right = '0'; iframe.style.bottom = '0'; iframe.style.width = '0'; iframe.style.height = '0';
document.body.appendChild(iframe);
iframe.contentDocument.open(); iframe.contentDocument.write(html); iframe.contentDocument.close();
setTimeout(() => {
html2canvas(iframe.contentDocument.body).then(canvas => {
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = `${exportId}.png`;
link.click();
iframe.remove();
});
}, 500);
}

function downloadSnapshotAsPDF(exportId) {
const snapshotStr = localStorage.getItem(`snapshot_${exportId}`);
if (!snapshotStr) { alert('Snapshot not found'); return; }
const snapshot = JSON.parse(snapshotStr);
const html = `<html><body><pre>${JSON.stringify(snapshot.payload, null, 2)}</pre></body></html>`;
const iframe = document.createElement('iframe');
iframe.style.position = 'fixed'; iframe.style.right = '0'; iframe.style.bottom = '0'; iframe.style.width = '0'; iframe.style.height = '0';
document.body.appendChild(iframe);
iframe.contentDocument.open(); iframe.contentDocument.write(html); iframe.contentDocument.close();
setTimeout(() => {
html2canvas(iframe.contentDocument.body).then(canvas => {
const imgData = canvas.toDataURL('image/png');
const pdf = new jspdf.jsPDF();
const imgProps = pdf.getImageProperties(imgData);
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = (imgProps.height * pdfWidth) / imgProps.width;
pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight);
pdf.save(`${exportId}.pdf`);
iframe.remove();
});
}, 500);
}

function listSnapshots() {
return JSON.parse(localStorage.getItem('snapshotIndex') || '[]');
}

window.exportProgress = {
buildProgressExportPayload,
downloadJson,
};
})();
generateSnapshot,
downloadSnapshotAsImage,
downloadSnapshotAsPDF,
listSnapshots
};})();

3 changes: 3 additions & 0 deletions my_progress.html
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,13 @@ <h2 style="margin-top:0">Export Progress</h2>
</p>

<div class="export-actions" style="display:flex; gap:12px; flex-wrap:wrap; justify-content:flex-start; margin-top:14px;">
<button id="generateSnapshotBtn" class="export-btn" type="button">Generate Snapshot</button>
<button id="downloadProgressJsonBtn" class="export-btn" type="button">Download JSON</button>
<button id="printProgressBtn" class="export-btn" type="button">Print / Save as PDF</button>
</div>

</div>

<!-- Print-only report container -->
<div id="printReport" class="print-report" style="display:none; margin-top:18px; text-align:left;">
<div style="display:flex; justify-content:space-between; gap:16px; align-items:flex-start; border-bottom:1px solid rgba(255,255,255,0.12); padding-bottom:12px;">
Expand Down
1 change: 1 addition & 0 deletions parents.html
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ <h2 style="margin-top:0">Topic-wise performance</h2>
<script src="progress.js"></script>
<script src="quizProgress.js"></script>
<script src="dashboardProgress.js"></script>
<script src="weeklyReport.js"></script>
</body>
</html>

1 change: 1 addition & 0 deletions quiz/motionquiz.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ <h2 id="result-title" tabindex="-1">🎉 Quiz Completed!</h2>
</section>
</div>

<script src="../accessibility.js"></script>
<script src="../quizProgress.js"></script>
<script src="../badges.js"></script>
<script src="adaptiveQuiz.js"></script>
Expand Down
12 changes: 7 additions & 5 deletions quiz/motionquiz.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,11 +283,13 @@ function startAdaptiveSession() {
adaptiveSteps[0] = adaptiveQuiz.takeNext();
}

document.addEventListener("DOMContentLoaded", () => {
document.addEventListener('DOMContentLoaded', () => {
window.__quizMotionStartedAt = Date.now();
document.getElementById("progress-bar").style.width = "0%";
// Initialize accessibility utilities
if (window.initQuizAccessibility) {
window.initQuizAccessibility();
}
document.getElementById('progress-bar').style.width = "0%";
startAdaptiveSession();
loadQuestion();
});


});
1 change: 1 addition & 0 deletions quiz/nlmquiz.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ <h2 id="result-title" tabindex="-1">🎉 Quiz Completed!</h2>
</section>
</div>

<script src="../accessibility.js"></script>
<script src="../quizProgress.js"></script>
<script src="../badges.js"></script>
<script src="adaptiveQuiz.js"></script>
Expand Down
12 changes: 7 additions & 5 deletions quiz/nlmquiz.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,11 +325,13 @@ function startAdaptiveSession() {
// Remaining questions will be determined as user answers.
}

document.addEventListener("DOMContentLoaded", () => {
document.addEventListener('DOMContentLoaded', () => {
// Initialize accessibility utilities
if (window.initQuizAccessibility) {
window.initQuizAccessibility();
}
window.__quizNlmStartedAt = Date.now();
document.getElementById("progress-bar").style.width = "0%";
document.getElementById('progress-bar').style.width = "0%";
startAdaptiveSession();
loadQuestion();
});


});
1 change: 1 addition & 0 deletions quiz/projectilequiz.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ <h2 tabindex="-1" id="result-heading">🎉 Quiz Completed!</h2>
</div>

<script src="../quizProgress.js"></script>
<script src="../accessibility.js"></script>
<script src="../badges.js"></script>
<script src="adaptiveQuiz.js"></script>
<script src="projectilequiz.js"></script>
Expand Down
4 changes: 4 additions & 0 deletions quiz/projectilequiz.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,10 @@ document.addEventListener("keydown", (e) => {
});

document.addEventListener("DOMContentLoaded", () => {
// Initialize accessibility utilities
if (window.initQuizAccessibility) {
window.initQuizAccessibility();
}
document.getElementById("progress-bar").style.width = "0%";
loadQuestion();

Expand Down
1 change: 1 addition & 0 deletions quiz/rayquiz.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ <h2 tabindex="-1" id="result-heading">🎉 Quiz Completed!</h2>
</div>

<script src="../quizProgress.js"></script>
<script src="../accessibility.js"></script>
<script src="../badges.js"></script>
<script src="adaptiveQuiz.js"></script>
<script src="rayquiz.js"></script>
Expand Down
4 changes: 4 additions & 0 deletions quiz/rayquiz.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,10 @@ document.addEventListener("keydown", (e) => {
});

document.addEventListener("DOMContentLoaded", () => {
// Initialize accessibility utilities
if (window.initQuizAccessibility) {
window.initQuizAccessibility();
}
document.getElementById("progress-bar").style.width = "0%";
loadQuestion();

Expand Down
Loading
Loading