v0.3.15-beta: kijken/werken-modus in de leerling-omgeving

- pupil.js: verwerkt de modus uit /my/assignment; "kijken" toont een
  meekijk-badge, blokkeert alle interactie en logt geen voortgang; "werken"
  (default) is het bestaande gedrag met voortgang-logging
- Poll-interval dynamisch: 5s in kijken-modus (wijzigingen van de leerkracht
  komen vlot door), 20s in werken-modus; moduswissel her-rendert direct
- teach.css: generieke kijken-overlay (pointer-events:none op de widget-body),
  werkt voor elk widgettype zonder eigen readonly-ondersteuning
- Nieuwe i18n-tekst pupilWatching (nl/en)
This commit is contained in:
Ramon 2026-07-14 15:51:20 +02:00
parent b13fe65da5
commit 873f8e0bee
4 changed files with 46 additions and 10 deletions

View file

@ -1 +1 @@
0.3.14-beta 0.3.15-beta

View file

@ -139,6 +139,16 @@
position:static; max-width:100%; box-shadow:0 6px 20px rgba(30,50,90,.12); position:static; max-width:100%; box-shadow:0 6px 20px rgba(30,50,90,.12);
} }
.pupil-card .widget-body{width:100%; height:100%;} .pupil-card .widget-body{width:100%; height:100%;}
/* kijken-modus: alle interactie geblokkeerd (generiek, werkt voor elk
widgettype zonder eigen readonly-ondersteuning) + zichtbaar meekijk-label */
.pupil-card.watch{position:relative;}
.pupil-card.watch .widget-body{pointer-events:none; user-select:none;}
.pupil-watch-badge{
position:absolute; top:8px; right:10px; z-index:2; pointer-events:none;
font-size:12.5px; font-weight:700; color:var(--muted);
background:var(--panel, rgba(255,255,255,.85)); border-radius:999px; padding:3px 10px;
box-shadow:0 2px 8px rgba(30,50,90,.15);
}
.pupil-empty{margin:auto; color:var(--muted); font-size:16px; font-weight:600;} .pupil-empty{margin:auto; color:var(--muted); font-size:16px; font-weight:600;}
/* floating add-button (FAB) */ /* floating add-button (FAB) */
#fab{ #fab{

View file

@ -2,7 +2,7 @@
"use strict"; "use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline). /* version shown until /api/version resolves (or if the fetch fails, e.g. offline).
Kept in sync by hand with the VERSION file at the repo root on every release. */ Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.3.14-beta"; const VERSION = "0.3.15-beta";
(function(){ (function(){
const tag = document.getElementById("verTag"); const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION; tag.textContent = "v"+VERSION;
@ -158,6 +158,7 @@ const I18N = {
roleSuper:"Overkoepelend beheerder", roleAdmin:"Schoolbeheerder", roleSuper:"Overkoepelend beheerder", roleAdmin:"Schoolbeheerder",
roleTeacher:"Groepsleiding", rolePupil:"Leerling", roleTeacher:"Groepsleiding", rolePupil:"Leerling",
pupilEmpty:"Je leerkracht heeft nog niets voor je klaargezet.", pupilEmpty:"Je leerkracht heeft nog niets voor je klaargezet.",
pupilWatching:"meekijken",
amUsers:"Gebruikers", amClasses:"Klassen", amSchools:"Scholen", amOverarching:"Overkoepelend", amUsers:"Gebruikers", amClasses:"Klassen", amSchools:"Scholen", amOverarching:"Overkoepelend",
amPickSchoolHint:"Kies eerst een school bij het tabblad “Overkoepelend”.", amPickSchoolHint:"Kies eerst een school bij het tabblad “Overkoepelend”.",
amNewSchool:" School", amNewClass:" Klas", amNewTeacher:" Groepsleiding", amNewSchool:" School", amNewClass:" Klas", amNewTeacher:" Groepsleiding",
@ -318,6 +319,7 @@ const I18N = {
roleSuper:"Overarching administrator", roleAdmin:"School administrator", roleSuper:"Overarching administrator", roleAdmin:"School administrator",
roleTeacher:"Group leader", rolePupil:"Pupil", roleTeacher:"Group leader", rolePupil:"Pupil",
pupilEmpty:"Your teacher hasn't set up anything for you yet.", pupilEmpty:"Your teacher hasn't set up anything for you yet.",
pupilWatching:"watching",
amUsers:"Users", amClasses:"Classes", amSchools:"Schools", amOverarching:"Overarching", amUsers:"Users", amClasses:"Classes", amSchools:"Schools", amOverarching:"Overarching",
amPickSchoolHint:"First pick a school in the “Overarching” tab.", amPickSchoolHint:"First pick a school in the “Overarching” tab.",
amNewSchool:" School", amNewClass:" Class", amNewTeacher:" Group leader", amNewSchool:" School", amNewClass:" Class", amNewTeacher:" Group leader",

View file

@ -2,19 +2,26 @@
klaargezet (via een toewijzing aan de klas of de leerling zelf), in klaargezet (via een toewijzing aan de klas of de leerling zelf), in
speelmodus (readonly) en zonder de sleep/vergroot/sluit-chrome van het bord. speelmodus (readonly) en zonder de sleep/vergroot/sluit-chrome van het bord.
Geen momentopname: /my/assignment zoekt live in het bord van de leerkracht, Geen momentopname: /my/assignment zoekt live in het bord van de leerkracht,
dus deze pagina pollt periodiek zodat nieuwe woorden vanzelf doorkomen. */ dus deze pagina pollt periodiek zodat nieuwe woorden vanzelf doorkomen.
Modus per toewijzing: "werken" (default, leerling werkt in de widget en
voortgang wordt gelogd) of "kijken" (alleen meekijken: alle interactie
geblokkeerd via een overlay, snellere poll zodat wijzigingen van de
leerkracht vlot doorkomen). */
const PUPIL_WIDGET_CATS = ["taal", "rekenen"]; const PUPIL_WIDGET_CATS = ["taal", "rekenen"];
const PUPIL_POLL_MS = { kijken: 5000, werken: 20000 };
const pupilView = document.getElementById("pupilView"); const pupilView = document.getElementById("pupilView");
let pupilPollTimer = null; let pupilPollTimer = null;
let pupilLastSig = null; let pupilLastSig = null;
let pupilMode = "werken";
function pupilAllowedDefs(){ function pupilAllowedDefs(){
return REGISTRY.filter(d => PUPIL_WIDGET_CATS.includes(d.cat)); return REGISTRY.filter(d => PUPIL_WIDGET_CATS.includes(d.cat));
} }
function renderPupilWidgets(widgets){ function renderPupilWidgets(widgets, mode){
pupilView.innerHTML = ""; pupilView.innerHTML = "";
const defs = pupilAllowedDefs(); const defs = pupilAllowedDefs();
const watch = mode === "kijken";
const usable = widgets const usable = widgets
.map(w => ({ w, def: defs.find(d => d.id === w.id) })) .map(w => ({ w, def: defs.find(d => d.id === w.id) }))
.filter(x => x.def); .filter(x => x.def);
@ -27,30 +34,46 @@ function renderPupilWidgets(widgets){
} }
usable.forEach(({ w, def }, idx)=>{ usable.forEach(({ w, def }, idx)=>{
const card = document.createElement("div"); const card = document.createElement("div");
card.className = "widget pupil-card"; card.className = "widget pupil-card" + (watch ? " watch" : "");
card.style.width = def.w + "px"; card.style.width = def.w + "px";
card.style.minHeight = def.h + "px"; card.style.minHeight = def.h + "px";
const body = document.createElement("div"); const body = document.createElement("div");
body.className = "widget-body"; body.className = "widget-body";
card.appendChild(body); card.appendChild(body);
if(watch){
const badge = document.createElement("div");
badge.className = "pupil-watch-badge";
badge.textContent = "👁 " + T("pupilWatching");
card.appendChild(badge);
}
pupilView.appendChild(card); pupilView.appendChild(card);
/* borden die vóór de invoering van stabiele widget-ids zijn opgeslagen /* borden die vóór de invoering van stabiele widget-ids zijn opgeslagen
hebben nog geen wid - dan is positie in de lijst de terugval-identiteit, hebben nog geen wid - dan is positie in de lijst de terugval-identiteit,
stabiel zolang de leerkracht de widgets op het bord niet herschikt */ stabiel zolang de leerkracht de widgets op het bord niet herschikt */
const widgetId = w.wid || (def.id + ":" + idx); const widgetId = w.wid || (def.id + ":" + idx);
def.mount(body, w.state || null, { readonly:true, onProgress: (p)=>{ /* in kijken-modus geen voortgang: de leerling doet zelf niets */
const opts = watch ? { readonly:true } : { readonly:true, onProgress: (p)=>{
api("/my/progress", { method:"POST", body: { widgetId, widgetType: def.id, ...p } }).catch(()=>{}); api("/my/progress", { method:"POST", body: { widgetId, widgetType: def.id, ...p } }).catch(()=>{});
}}); }};
def.mount(body, w.state || null, opts);
}); });
} }
async function pupilPoll(){ async function pupilPoll(){
try{ try{
const res = await api("/my/assignment"); const res = await api("/my/assignment");
const sig = JSON.stringify(res.widgets || []); const mode = res.mode === "kijken" ? "kijken" : "werken";
/* modus in de signature zodat een moduswissel ook zonder widget-wijziging
een her-render (en dus de juiste overlay) oplevert */
const sig = mode + "|" + JSON.stringify(res.widgets || []);
if(sig !== pupilLastSig){ if(sig !== pupilLastSig){
pupilLastSig = sig; pupilLastSig = sig;
renderPupilWidgets(res.widgets || []); renderPupilWidgets(res.widgets || [], mode);
}
if(mode !== pupilMode){
pupilMode = mode;
clearInterval(pupilPollTimer);
pupilPollTimer = setInterval(pupilPoll, PUPIL_POLL_MS[mode]);
} }
}catch(e){ /* stille mislukking - volgende poll probeert opnieuw */ } }catch(e){ /* stille mislukking - volgende poll probeert opnieuw */ }
} }
@ -58,9 +81,10 @@ async function pupilPoll(){
function startPupilView(){ function startPupilView(){
document.body.classList.add("pupil-mode"); document.body.classList.add("pupil-mode");
pupilLastSig = null; pupilLastSig = null;
pupilMode = "werken";
pupilPoll(); pupilPoll();
clearInterval(pupilPollTimer); clearInterval(pupilPollTimer);
pupilPollTimer = setInterval(pupilPoll, 20000); pupilPollTimer = setInterval(pupilPoll, PUPIL_POLL_MS[pupilMode]);
} }
function stopPupilView(){ function stopPupilView(){
document.body.classList.remove("pupil-mode"); document.body.classList.remove("pupil-mode");