All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 8s
151 lines
5.8 KiB
JavaScript
151 lines
5.8 KiB
JavaScript
/* widget: Timer */
|
|
/* =========================================================
|
|
Timer widget — digits, visual "kijkklok" disc, or both
|
|
==========================================================*/
|
|
function mountTimer(root, initState){
|
|
const DIAL = 3600; // the disc is a 60-minute dial, like a real kijkklok
|
|
let total = 300, left = 300, iv = null,
|
|
mode = "both", // dig | pie | both
|
|
snd = true;
|
|
if(initState){
|
|
total = initState.total || 300; left = total;
|
|
mode = initState.mode || "both";
|
|
snd = initState.snd !== false;
|
|
}
|
|
root.innerHTML = `
|
|
<div class="timer m-both">
|
|
<div class="topts">
|
|
<button class="bsnd ticon"></button>
|
|
<div class="tseg">
|
|
<button data-m="dig">🔢</button>
|
|
<button data-m="pie">🔴</button>
|
|
<button data-m="both">🔢🔴</button>
|
|
</div>
|
|
</div>
|
|
<div class="tstage">
|
|
<svg class="tpie" viewBox="0 0 150 150">
|
|
<circle cx="75" cy="75" r="70" fill="#eef3f9" stroke="#d5dfeb" stroke-width="2"/>
|
|
<g class="ticks"></g>
|
|
<path class="parc" fill="var(--red)"/>
|
|
<circle class="knob" r="7" fill="#fff" stroke="var(--red)" stroke-width="3"/>
|
|
<circle cx="75" cy="75" r="6" fill="#fff" stroke="#b9c7d9" stroke-width="2"/>
|
|
</svg>
|
|
<div class="tdisp">05:00</div>
|
|
</div>
|
|
<div class="trow presets"></div>
|
|
<div class="trow main">
|
|
<button class="bstart"></button>
|
|
<button class="breset"></button>
|
|
</div>
|
|
<div class="thint"></div>
|
|
</div>`;
|
|
const box = root.querySelector(".timer"),
|
|
svg = root.querySelector(".tpie"),
|
|
disp = root.querySelector(".tdisp"),
|
|
arc = root.querySelector(".parc"),
|
|
knob = root.querySelector(".knob"),
|
|
bstart = root.querySelector(".bstart"),
|
|
breset = root.querySelector(".breset"),
|
|
bsnd = root.querySelector(".bsnd"),
|
|
presets = root.querySelector(".presets"),
|
|
hint = root.querySelector(".thint");
|
|
const fmt = s => String(Math.floor(s/60)).padStart(2,"0")+":"+String(s%60).padStart(2,"0");
|
|
|
|
/* minute tick marks (every 5 min) */
|
|
const ticks = root.querySelector(".ticks");
|
|
for(let i=0;i<12;i++){
|
|
const a = i*30*Math.PI/180;
|
|
const l = document.createElementNS("http://www.w3.org/2000/svg","line");
|
|
l.setAttribute("x1", 75+62*Math.sin(a)); l.setAttribute("y1", 75-62*Math.cos(a));
|
|
l.setAttribute("x2", 75+69*Math.sin(a)); l.setAttribute("y2", 75-69*Math.cos(a));
|
|
l.setAttribute("stroke", "#b9c7d9"); l.setAttribute("stroke-width", i%3===0?2.5:1.5);
|
|
ticks.appendChild(l);
|
|
}
|
|
|
|
function drawPie(){
|
|
const a = Math.min(360, left/DIAL*360);
|
|
const rad = a*Math.PI/180;
|
|
const kx = 75 + 63*Math.sin(rad), ky = 75 - 63*Math.cos(rad);
|
|
knob.setAttribute("cx", kx.toFixed(2)); knob.setAttribute("cy", ky.toFixed(2));
|
|
if(a<=0){ arc.setAttribute("d",""); return; }
|
|
if(a>=359.99){ arc.setAttribute("d","M75,5 A70,70 0 1 1 74.99,5 Z"); return; }
|
|
const x = 75 + 70*Math.sin(rad), y = 75 - 70*Math.cos(rad);
|
|
arc.setAttribute("d",`M75,75 L75,5 A70,70 0 ${a>180?1:0} 1 ${x.toFixed(2)},${y.toFixed(2)} Z`);
|
|
}
|
|
function draw(){
|
|
disp.textContent = fmt(left);
|
|
disp.classList.toggle("warn", left<=10 && left>0 && iv!==null);
|
|
drawPie();
|
|
}
|
|
function labels(){
|
|
bstart.textContent = (iv ? "⏸ "+T("pause") : "▶ "+T("start"));
|
|
breset.textContent = "↺ "+T("reset");
|
|
presets.querySelectorAll("button").forEach(b=>b.textContent = b.dataset.m+" "+T("min"));
|
|
root.querySelectorAll(".tseg button").forEach(b=>{
|
|
const key = b.dataset.m==="dig" ? "tmDigits" : b.dataset.m==="pie" ? "tmVisual" : "tmBoth";
|
|
b.title = T(key);
|
|
b.classList.toggle("on", b.dataset.m===mode);
|
|
});
|
|
bsnd.textContent = snd ? "🔔" : "🔕";
|
|
bsnd.title = T("tmSound");
|
|
bsnd.classList.toggle("on", snd);
|
|
box.className = "timer m-"+mode + (box.classList.contains("flash")?" flash":"");
|
|
hint.textContent = mode==="dig" ? "" : T("tmHint");
|
|
}
|
|
[1,5,10,15,30].forEach(m=>{
|
|
const b = document.createElement("button");
|
|
b.dataset.m = m;
|
|
b.addEventListener("click", ()=>{ stop(); total=left=m*60; draw(); labels(); });
|
|
presets.appendChild(b);
|
|
});
|
|
root.querySelectorAll(".tseg button").forEach(b=>{
|
|
b.addEventListener("click", ()=>{ mode = b.dataset.m; labels(); });
|
|
});
|
|
bsnd.addEventListener("click", ()=>{ snd=!snd; labels(); });
|
|
|
|
/* drag on the disc to set the time */
|
|
function angleFromEvent(ev){
|
|
const r = svg.getBoundingClientRect();
|
|
const px = (ev.clientX - r.left)*150/r.width - 75;
|
|
const py = (ev.clientY - r.top)*150/r.height - 75;
|
|
return (Math.atan2(px, -py)*180/Math.PI + 360) % 360;
|
|
}
|
|
svg.addEventListener("pointerdown", e=>{
|
|
e.preventDefault();
|
|
svg.setPointerCapture(e.pointerId);
|
|
const setFrom = ev=>{
|
|
const mins = Math.min(60, Math.max(1, Math.round(angleFromEvent(ev)/6)));
|
|
total = left = mins*60;
|
|
draw();
|
|
};
|
|
setFrom(e);
|
|
const move = ev=>setFrom(ev);
|
|
const up = ()=>{ svg.removeEventListener("pointermove",move); svg.removeEventListener("pointerup",up); };
|
|
svg.addEventListener("pointermove",move);
|
|
svg.addEventListener("pointerup",up);
|
|
});
|
|
|
|
function stop(){ if(iv){ clearInterval(iv); iv=null; } }
|
|
bstart.addEventListener("click", ()=>{
|
|
if(iv){ stop(); }
|
|
else if(left>0){
|
|
iv = setInterval(()=>{
|
|
left--;
|
|
if(left<=0){
|
|
left=0; stop();
|
|
if(snd) sndWin();
|
|
box.classList.add("flash");
|
|
setTimeout(()=>box.classList.remove("flash"), 2200);
|
|
labels();
|
|
}
|
|
draw();
|
|
},1000);
|
|
}
|
|
labels(); draw();
|
|
});
|
|
breset.addEventListener("click", ()=>{ stop(); left=total; draw(); labels(); });
|
|
document.addEventListener("langchange", labels);
|
|
labels(); draw();
|
|
return { getState: ()=>({total, mode, snd}) };
|
|
}
|
|
|