(function (){ // var wsURL = "wss://tmp.iekei.org/jstype"; var wsURL = "wss://" + location.host + "/jtserv"; var ws; // for WebSocket Object var textlist = []; var nText = nText||4; var nLine = nLine||3; var area = []; var team = {}, teamnames = new Set(), tmsel, nmsel, txsel; var mystate = {}; var infoBox; var loginhead, prompt, input, tmpkey; var singleTextMode = true; // // https://developer.mozilla.org/ja/docs/Web/API/Element/classList function setElementIDText(id, text) { document.getElementById(id).innerText = text; } function loadTextToElement1(text) { console.log("elm1="+mystate.elm1); mystate.elm1.textContent = text[0]; mystate.elm2.textContent = text.substr(1); resetState(); } function loadTextToElement(hash, elm1, elm2) { sendJSONtoServer({ "gettext": hash.file, "lines": nLine, "fill": 60 }); mystate.elm1 = elm1; mystate.elm2 = elm2; } function updateMemberOption(e) { let tn = tmsel.value; console.log(nmsel.childNodes); // We should repeat remove lastChild when deleting all children // https://into-the-program.com/removechild/ while (nmsel.lastChild) nmsel.removeChild(nmsel.lastChild); for (let mem of team[tn]) { let o = document.createElement("option") o.textContent = mem.name; nmsel.appendChild(o); o.selected = true; } } function sendJSONtoServer(json) { if (ws.readyState == WebSocket.OPEN) { // ==1 ws.send(JSON.stringify(json)); } } function sendMynameToServer() { sendJSONtoServer({"name": nmsel.value, "team": tmsel.value}); } function loadTeamList(file, elm) { fetch(file).then((resp) => { if (resp.ok) return resp.text(); }).then((txt) => { let teamcsv = new CSV(txt, {header: true}).parse(); let tmpa; for (let row of teamcsv) { // Collect all team members if (!team[row.teamname]) team[row.teamname]=[]; team[row.teamname].push({uid: row.uid, name: row.name}); } if (tmsel) { for (let tn of Object.keys(team)) { let o = document.createElement("option") o.textContent = tn; tmsel.appendChild(o); } updateMemberOption(); tmsel.addEventListener("change", updateMemberOption); nmsel.addEventListener("change", sendMynameToServer); console.log(Object.keys(team)); } }); } // Typing engine var pos=0, nArea=0; function switchToArea(n) { if (singleTextMode) { for (let i=0; i<area.length; i++) { area[i].style.display = (i==n ? "block" : "none"); } } area[nArea].classList.remove("current"); nArea = n; if (nArea < area.length) { area[nArea].classList.add("current"); } } function startTrr(time) { mystate.start = time; sendJSONtoServer({ "text": null, // XXXXX not yet "start": time }); info.classList.add("go"); infoBox.innerText = "GO!!"; mystate.miss = 0; document.body.addEventListener("click", _entryFocus); } function commentScore(score) { if (score < 10) return "努力あるのみ"; else if (score < 50) return "まず正しい指使いを覚えなさい"; else if (score < 100) return "見ながら打っているようではダメよ"; else if (score < 160) return "ここからが勝負よ"; else if (score < 200) return "あと一息で業界標準の200点よ"; else if (score < 220) return "業界標準ね"; else if (score < 300) return "業界目標の300点までもう少し!"; else if (score < 400) return "業界一流の400点は行っておかないと"; else return "業界一流ね. ここから先は自分との戦いよ"; } function updateScoreField() { let text = textlist[nArea]["file"], data = mystate.scoreinfo[text]; setElementIDText("name", mystate.scoreinfo.gecos); if (data) { let st = mystate.scoreinfo.steptrial[text][data.step]; setElementIDText("step", data.step); setElementIDText("target", 10*(data.step+1)); setElementIDText("hs", data.highscore); setElementIDText("trial", data.trial); setElementIDText("strial", st); } else { for (let i of ["step", "target", "hs", "trial", "strial"]) setElementIDText(i, ""); } } function finishTrr(data) { document.body.removeEventListener("click", _entryFocus); setElementIDText("time", data.time+"s"); setElementIDText("score", Math.max(Math.round(data.score), 0)); setElementIDText("types", mystate.sum); setElementIDText("speed", data.speed); setElementIDText("comment", data.message+"\n(TEXT選択ボタンで再挑戦)"); mystate.scoreinfo = data.scoreinfo; updateScoreField(); setElementIDText("info", "GOAL!!"); } function sendFinishTrr() { let finish = Date.now()/1000; let elapse = finish - mystate.start, sum=0; mystate.sum = sum = area[nArea].textContent.length+1; console.log("nArea="+nArea); console.log("LEN="+sum); console.log("elapse="+elapse); sendJSONtoServer({ "text": textlist[nArea]["file"], "start": mystate.start, "finish": finish, "miss": mystate.miss, "types": sum, "trrmode": mystate.trrmode }); } function trr(e) { e.preventDefault(); let done = area[nArea].childNodes[0], curs = area[nArea].childNodes[1], text = area[nArea].childNodes[2], inp = e.target; let str, len=done.textContent.length; if (e.keyCode == 13) { console.log("ENTER"); str = "\n"; } else { str = inp.value; } if (mystate.finish > 0) { return; } else if (mystate.start == 0) { if (e.keyCode == 8) { // First Hit of [BS] Start Self-training startTrr(Date.now()/1000); mystate.singleUserMode = true; return; } else { info.innerText = "まだだよ(BSキーで練習スタート)"; } } else { if (str.length > 10) { alert("あら、なにかおかしいわね。"); alert("頭を冷やして明日またやりましょう。"); alert("もしかして日本語入力をONにしてしまったとかかしら?"); inp.value = "" resetState(); return; } while (str > "") { if (str[0] == curs.textContent) { if (text.textContent > "") { if (mystate.missflag) { done.innerHTML += '<span class="miss">'+str[0]+'</span>'; console.log("done="+done.innerHTML); } else { console.log("X"+str); done.innerHTML += str[0]; console.log("DONE="+done.innerHTML); } mystate.missflag = false; curs.textContent = text.textContent[0]; } else { curs.textContent = ""; // Prevent adding newline } pos++; text.textContent = text.textContent.substr(1); } else { console.log("MISSSSSSSS"+str); mystate.miss++; mystate.missflag = true; } str = str.substr(1); } sendJSONtoServer({"types": done.textContent.length}); setElementIDText("miss", mystate.miss); if (len>0) { let ratio = " "+Math.round(1000*mystate.miss/len)/10+"%"; ratio = ratio.substr(-6); setElementIDText("ratio", ratio); } } inp.value = ""; if (curs.textContent == "") { mystate.finish = Date.now()/1000; if (nArea+1 < area.length && !mystate.singleUserMode) { switchToArea(++nArea); } else { // Finished!! // finishTrr(); sendFinishTrr(); } } } function selTextAndSend(e) { let n = e.target.value; for (let j=0; j<area.length; j++) { if (n==j) area[j].classList.add("mytext"); else area[j].classList.remove("mytext"); } console.log("TL="+textlist) sendJSONtoServer({"settext": n}); // updateScoreField(); } function clearText() { let stage = area[nArea]; stage.querySelector(".done").textContent = ""; stage.querySelector(".cursor").textContent = ""; stage.querySelector(".text").textContent = ""; } function reloadText(n) { // Reload text(n) to typing field let stage = area[n], dne = stage.querySelector(".done"), cur = stage.querySelector(".cursor"), txt = stage.querySelector(".text"); console.log("stage="+stage); console.log("cur="+cur); console.log("txt="+txt); dne.textContent = ""; loadTextToElement(textlist[n], cur, txt); switchToArea(n); setTimeout(()=>{entry.focus();}, 100); } function reloadTextByBtn(e) { let n = e.target.value; if (n<0 || n>=textlist.length) return; reloadText(n); } function xxx_prepareStage() { fetch("TEXT").then((resp) => { if (resp.ok) return resp.text(); }).then(async (text) => { // Should be async func for serial loading let tl0 = text.split("\n"), tl=[], f; for (f of tl0) if (f!="\n" && f!="") tl.push(f); // Eliminate non-filenames for (f of tl) { // console.log("LOADING "+f); let csv = f.split(","), file=csv[0], title=csv[1]; if (false) { // XXXXXXXXXXXXXXXXXXXXXXXXXXXX await fetch(file).then((r)=>{ // Load sequentially if (r.ok) return r.text(); }).then((text) => { textlist.push({"file":file, "title":title, "text":text}); if (textlist.length == tl.length) createStage(); }); } else { textlist.push({"file":file, "title":title}); } } createStage(); if ("scoreinfo" in mystate) { txsel.querySelector("label+label>input").click(); //Fake update by click() setTimeout(()=>{txsel.querySelector("input").click()}, 100); // setElementIDText("hs", hs.text||"0") } }); } function prepareStage(textfiles) { if (textlist.length > 1) return; // Maybe 2nd time textlist = textfiles; createStage(); if ("scoreinfo" in mystate) { txsel.querySelector("label+label>input").click(); //Fake update by click() setTimeout(()=>{txsel.querySelector("input").click()}, 100); // setElementIDText("hs", hs.text||"0") } let entry = document.getElementById("entry"); entry.focus(); entry.addEventListener("keyup", trr, false); } function createStage() { let stage = document.getElementById("typing"); // for (let i=0; i<nText; i++) for (let i=0; i<textlist.length; i++) { let pre = document.createElement("pre"), done = document.createElement("span"), curs = document.createElement("span"), text = document.createElement("span"), rbtn = document.createElement("input"); let texthash = textlist[i]; done.classList.add("done"); curs.classList.add("cursor"); text.classList.add("text"); stage.appendChild(pre); pre.appendChild(done); pre.appendChild(curs); pre.appendChild(text); pre.setAttribute("contentEditable", false); area.push(pre) // console.log("area==="+area); let textlabel = document.createElement("button"); textlabel.textContent = texthash.title; //textlabel = document.createTextNode(texthash.file); loadTextToElement(texthash, curs, text) rbtn.setAttribute("type", "radio"); rbtn.setAttribute("name", "text"); rbtn.setAttribute("value", i); rbtn.addEventListener("change", selTextAndSend); rbtn.addEventListener("click", reloadTextByBtn); let label = document.createElement("label"); // https://qiita.com/sola-msr/items/bdec752da00c5ab677b3 textlabel.style.pointerEvents = "none"; label.appendChild(rbtn); label.appendChild(textlabel); txsel.appendChild(label); } switchToArea(0); } function filterSameTeam(e) { e.stopPropagation(); let tbl = document.getElementById("ranking-table"); let team = e.target.textContent, body = document.body; mystate.collapsed = mystate.collapsed ? false : team; let visible = mystate.collapsed ? "collapse" : "visible", rows = document.querySelectorAll("#ranking-table tr"); for (let i=1; i<rows.length; i++) { // i==0 is header line, skip it let tr = rows[i]; tr.style.visibility = tr.classList.contains(team) ? "visible" : visible; } e.target.scrollIntoView({behavior: "smooth", block: "center"}); if (mystate.collapsed) tbl.addEventListener("click", filterSameTeam); else tbl.removeEventListener("click", filterSameTeam); } function showRank(list) { mystate.ranklist = list; mystate.collapsed = false; // user, max(score), max(step), count(score), sum(time)/60, max(at) let tbl = document.getElementById("ranking-table"), me, teamcol, rankinteam = {}; if (false) { for (let i=0; i<40; i++) { let tr = document.createElement("tr") tr.innerHTML="<tr><th>"+i+"</th></tr>"; tbl.appendChild(tr); } } for (let i in list) { let tr = document.createElement("tr"), rank = document.createElement("th"); if (i==0) { // Header row if (list.mode == null) { for (let c in list[i]) if (list[i][c] == "Team") { teamcol = c break; } } } else { rank.textContent = parseInt(i); // for contents rows } tr.appendChild(rank); for (let j=0; j<list[i].length; j++) { let td = document.createElement(i==0?"th":"td"); td.textContent = list[i][j]; tr.appendChild(td); if (j == teamcol) { // Add inTeam ranking hook let tm = list[i][j]; tr.classList.add(tm); td.addEventListener("click", filterSameTeam); if (!rankinteam[tm]) rankinteam[tm] = 0; td.title = (++rankinteam[tm]) + " in "+tm; } } tbl.appendChild(tr); if (mystate.user == list[i][0]) me = tr; } if (me) { me.classList.add("me"); setTimeout(() => { me.scrollIntoView({behavior: "smooth", block: "center"}); console.log(me); }, 1000); } } function downloadCSV(ev) { ev.stopPropagation(); if (!mystate.ranklist) return; let list = mystate.ranklist, mode = mystate.rankmode, team = mystate.collapsed, str="", text = textlist[nArea]["file"], file = text+(mode ? "-"+mode : "") + (team?"_"+team:"")+".csv" if (team) { str = "Text" + list[0].join(",") + "\n"; let tbl = document.getElementById("ranking-table"); for (let row of tbl.querySelectorAll("tr")) { if (row.classList.contains(mystate.collapsed)) { str += text; for (let col of row.querySelectorAll("td")) str += ","+col.textContent; str += "\n"; } } } else { for (let i in list) { str += (i==0?"Text":text)+"," + list[i].join(",") + "\n"; } } let bom = new Uint8Array([0xEF, 0xBB, 0xBF]); var a = document.createElement("a"); var csv = new Blob([bom, str], {type: "text/csv"}); var uri = URL.createObjectURL(csv); a.download = file; a.href = uri; document.body.appendChild(a); a.click(); document.body.removeChild(a); } function getServerMessage(e) { console.log("Get: "+e.data); let data = JSON.parse(e.data); if (data.myid) { mystate.myid = data.myid; //infoBox.innerText = "Ready"; resetState(); } else if (data.start) { startTrr(data.start); } else if (data.disable) { for (let lbl of txsel.childNodes) { let inp = lbl.firstChild; console.log(inp.tagName); console.log(inp.value==data.disable); if (!inp.tagName.match(/input/i)) continue; if (inp.value==data.disable) { inp.disabled = true; lbl.classList.add("disabled"); } else { inp.disabled = false; lbl.classList.remove("disabled"); } } } else if (data.tmpkey) { input.disabled = false; if (data.email) { loginhead.innerText = "Sent passcode to "+data.email; prompt.innerText = "Passcode" tmpkey = data.tmpkey; // Don't save tmp-skey mystate.user = data.user; input.value = ""; input.setAttribute("type", "password"); } else { loginhead.innerText = "Input Your Login ID"; } input.focus(); } else if ("step" in data) { finishTrr(data); } else if (data.fail) { input.disabled = false; switch (data.fail) { case "fail": loginhead.innerHTML = "Invalid passcode<br>Try again "; logdiv.classList.remove("login2"); input.value = ""; input.focus(); break; case "byebye": loginhead.innerHTML = "Too many login failure... Reloading "; setTimeout(logout, 2000); break; case "nokey": // alert("Ooops - session forcibly been timeout") _reset(); break; case "overlimit": alert(data.message); break; } } else if (data.user && data.skey) { console.log("You are "+data.user); prepareStage(data.textfiles); let logdiv = document.querySelector("div.login"); localStorage.setItem("trrSkey", data.skey); localStorage.setItem("trrUser", data.user); tmpkey = null; // Reset temporary skey console.log("LSK="+localStorage.getItem("trrSkey")); console.log("LSU="+localStorage.getItem("trrUser")); mystate.user = data.user; mystate.scoreinfo = data.scoreinfo; setElementIDText("user", data.user); if (logdiv) { logdiv.setAttribute("class", "login2"); setTimeout(() => { logdiv.remove(); console.log("BYEBYE"); }, 1000); } } else if (data.scoreinfo) { mystate.scoreinfo = data.scoreinfo; updateScoreField(); } else if (data.ranking) { console.log("RANKING="+JSON.stringify(data.ranking)); showRank(data.ranking); } else if (data.yourtext) { console.log("YT="+data.yourtext); loadTextToElement1(data.yourtext); } else if (data.css) { let csslink = document.createElement("link"); csslink.setAttribute("rel", "stylesheet"); csslink.setAttribute("href", data.css); console.log(csslink); document.querySelector("head").appendChild(csslink); } else { console.log("message: "+data.message); } } function ranking(ev) { var mode = null; switch (ev.target.id) { case "ranking": mode = null; break; default: mode = ev.target.id.replace("ranking:", ""); break; } mystate["rankmode"] = mode; var div = document.createElement("div"); div.classList.add("login2"); setTimeout(()=>{div.setAttribute("class", "ranking")}, 1000); var p1 = document.createElement("p"); p1.className = "c t"; p1.innerHTML = "<button>Back to stage</button><p>"+ (mode||"Total")+"</p>"; var p2 = document.createElement("p"); p2.className = "c t"; var b2 = document.createElement("button"); b2.innerText = "Download CSV"; b2.addEventListener("click", downloadCSV, false); p2.appendChild(b2); var d2 = document.createElement("div"); d2.classList.add("tbl"); var tbl = document.createElement("table"); tbl.id = "ranking-table"; tbl.classList.add("ranking"); div.appendChild(p1); div.appendChild(d2); div.appendChild(p2); d2.appendChild(tbl); document.body.appendChild(div); if (history.pushState) { let h = location.href.replace(/#.*/, '')+"#ranking"; history.pushState({url: h}, null, h); window.addEventListener("popstate", (e) => { if (div) { div.remove(); div = null; } if (mystate.start && mystate.start > 0) { clearText(); resetState(); } _entryFocus(); }, false); } let text = textlist[nArea].file; mystate.ranklist = null; // Clear ranking list let request = {ranking: text}; if (mode) request["mode"] = mode; sendJSONtoServer(request); div.addEventListener("click", ()=>{history.back();}, false); div.addEventListener("keydown", ()=>{history.back();}, false); } function resetState() { mystate.start = mystate.miss = mystate.finish = 0; for (id of ["miss", "ratio", "types", "time", "score"]) { setElementIDText(id, ""); } if (area[nArea] && area[nArea].textContent && area[nArea].textContent > "") setElementIDText("comment", "自主練は BS で開始よ。"); else setElementIDText("comment", "まずTEXTを選択するところからよ。"); setElementIDText("info", "Ready..."); } function tryLogin() { let skey = localStorage.getItem("trrSkey"); // alert(skey); if (skey && skey > "") { let user = mystate.user||localStorage.getItem("trrUser"); console.log("Sending"+skey); sendJSONtoServer({"user": user, "skey": skey}); } } function _reset(e) { localStorage.clear(); mystate.user = ""; tmpkey = null; } function initLogin() { function loginProcedure(json) { } function _send(e) { if (prompt && prompt.innerText > "" && input && input.value > "") { let user = mystate.user||localStorage.getItem("trrUser"), skey = localStorage.getItem("trrSkey"); let j = {} j[prompt.innerText] = input.value; if (user && user>"") j.user = user; if (tmpkey && tmpkey>"") j.tmpkey = tmpkey; input.value = "" sendJSONtoServer(j); input.disabled = true; } }; tryLogin(); input.addEventListener( "keypress", (e) => {if (e.key=='Enter') _send(e);}); let login = document.getElementById("login"); if (login) { login.addEventListener("click", _send); document.getElementById("reset").addEventListener("click", _reset); } } function logout() { _reset(); location.reload(); } function trrmode(ev) { document.body.classList.remove(mystate["trrmode"]); document.body.classList.add(mystate["trrmode"] = ev.target.value); _entryFocus(); } function setupTrrMode() { var modesel = document.getElementById("trrmode"); modesel.addEventListener("change", trrmode); modesel.options[0].selected = true; } var wsInitRetry = 10; function wsInit() { loginhead.textContent = "Connecting Server....."+wsInitRetry; input.disabled = true; // Start with input prompt disabled ws = new WebSocket(wsURL); ws.onerror = (e) => { console.log("Oh..."); if (--wsInitRetry > 0) setTimeout(wsInit, 5000); }; infoBox.removeEventListener("click", wsInit); var typing = document.getElementById("typing"); ws.onopen = function () { console.log("WS-OK"); input.disabled = false; input.focus(); infoBox.classList.remove("warn"); loginhead.textContent = "jsTRR"; initLogin(); }; ws.onmessage = getServerMessage; ws.onclose = function () { setElementIDText("info", "No Server Connection. Click to reconnect"); infoBox.classList.add("warn"); infoBox.addEventListener("click", wsInit); }; } function _entryFocus() { if (entry) entry.focus(); } function init() { let btn; prompt = document.getElementById("prompt"); input = document.getElementById("inputvalue"); console.log("prom="+prompt.innerText+", val="+input.value); tmsel = document.querySelector("select[name=team]"); nmsel = document.querySelector("select[name=name]"); txsel = document.getElementById("text"); loginhead = document.getElementById("loginhead"); for (let btn of document.querySelectorAll("button")) { if (btn.id.match(/^ranking/)) btn.addEventListener("click", ranking); } let entry = document.createElement("input"); entry.id = "entry"; entry.type = "text"; document.body.appendChild(entry); infoBox = entry.insertAdjacentHTML( "afterend", '<p class="info" id="info"><p>'); infoBox = document.getElementById("info"); document.getElementById("logout").addEventListener("click", logout); setupTrrMode() wsInit(); } document.addEventListener("DOMContentLoaded", init, false); //loadTeamList("teams.csv"); })();