Newer
Older
jstrr / jstrr.js
@HIROSE Yuuji HIROSE Yuuji on 24 Nov 2021 19 KB Do not repeat text-button loading
(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||2;
    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 xxx_fillText(text, column) {
	var tx = text.replace(/\n/g, " ").replace(/\s+/, " ");
	var array = [], pos=0, thisline="";
	var re = /(\S+\s+)/g;
	var found = tx.split(" ");
	console.log("found LEN="+found.length);
	for (w of found) {
	    //console.log("tl="+w+", len="+w.length);
	    //console.log("thisllen="+thisline.length);
	    if (thisline.length + w.length < column) {
		if (w.endsWith(".")) w += " ";
		thisline += w+" ";
		// console.log("THISLINE=["+thisline+"]");
	    } else {
		array.push(thisline.trim());
		//console.log("ArrayLen="+array.length);
		//console.log("Array=["+array.join("/")+"]");
		thisline = "";
	    }
	}
	if (thisline > "") array.push(thisline.trim());
	return array;
    }
    function xxx_loadTextToElement0(hash, elm1, elm2) {
	let txt = hash.text, subtxt="";
	let array = txt.split("\n"),
	    b = 5,		// number of buffer lines
	    number = Math.max(0, array.length-nLine-b);
	let s;
	let trial = 10;
	if (!txt.match(/Article/)) trial = 1;
	while (trial-- > 0 && !subtxt.match(/^(|\.\s+)[0-9A-Z]/)) {
	    s = Math.floor(Math.random()*number);
	    subtxt = array.slice(s, s+nLine+b).join("\n");
	    // console.log("subtxt=["+subtxt+"]");
	}
	if (txt[0] == "#") {	// Maybe source of scripting languege
	    array = subtxt.split("\n");
	} else {
	    if (!subtxt.match(/^[0-9A-Z]/)) {
		console.log("REP!!!!!!!! "+subtxt.substr(0, 10));
		subtxt.replace(/^.*?\.  */, "");
		console.log("DONE: "+subtxt.substr(0, 10));
	    }
	    array = fillText(subtxt, 40);
	}
	if (array.length > nLine) {
	    txt = "";
	    for (let i=0; i<nLine; i++) {
		txt += array[i].trim()+"\n";
		// txt += array[i].trim()+"\n";
		txt.trim();
	    }
	}
	elm1.textContent = txt[0];
	elm2.textContent = txt.substr(1);
	resetState();
    }
    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": 40
	});
	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
	});
    }
    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("頭を冷やして明日またやりましょう。");
		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 reloadText(e) {
	let n = e.target.value;
	if (n<0 || n>=textlist.length) return;
	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 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", reloadText);
	    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 showRank(list) {
	// user, max(score), max(step), count(score), sum(time)/60, max(at)
	let tbl = document.getElementById("ranking-table"), me;
	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");
	    rank.textContent = parseInt(i)+1;
	    tr.appendChild(rank);
	    for (let j=0; j<list[i].length; j++) {
		let td = document.createElement("td");
		td.textContent = list[i][j];
		tr.appendChild(td);
	    }
	    tbl.appendChild(tr);
	    console.log("MU="+mystate.user+", "+list[i][0]);
	    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 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) {
	    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) {
	    
	    switch (data.fail) {
	    case "fail":
		loginhead.innerHTML = "Invalid passcode<br>Try again ";
		logdiv.classList.remove("login2");
		input.value = "";
		input.focus();
		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) {
	    //console.log("DSSSS="+JSON.stringify(data.scoreinfo));
	    //console.log("STRI="+JSON.stringify(data.scoreinfo.steptrial));
	    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 {
	    console.log("message: "+data.message);
	}
    }
    function ranking(ev) {
	var div = document.createElement("div");
	div.classList.add("login2");
	setTimeout(()=>{div.setAttribute("class", "ranking")}, 1000);
	div.innerHTML = "<p class=\"c t\"><button>Back to stage</button></p>"
	var d2 = document.createElement("div");
	d2.classList.add("tbl");
	var tbl = document.createElement("table");
	tbl.id = "ranking-table";
	tbl.classList.add("ranking");
	tbl.innerHTML = "<tr>\
<th>Rank</th><th>User</th><th>Score</th><th>Step</th>\
<th>Try</th><th>Minutes</th><th>Time</th></tr>"
	div.appendChild(d2);
	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;
		}
	    }, false);
	}
	let text = textlist[nArea].file;
	sendJSONtoServer({ranking: text});
	div.addEventListener("click", ()=>{history.back();});
	div.addEventListener("keydown", ()=>{history.back();}, false);
	// div.querySelector("button").focus();
    }
    function resetState() {
	mystate.start = mystate.miss = mystate.finish = 0;
	for (id of ["miss", "ratio", "types", "time", "score"]) {
	    setElementIDText(id, "");
	}
	setElementIDText("comment", "自主練は BS で開始よ。");
	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) {
	    loginhead = document.getElementById("loginhead");
	    prompt = document.getElementById("prompt");
	    input = document.getElementById("inputvalue");
	    console.log("prom="+prompt.innerText+", val="+input.value);
	    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);
	    }
	};
	tryLogin();
	document.getElementById("inputvalue").addEventListener(
	    "keypress", (e) => {if (e.key=='Enter') _send(e);});
	document.getElementById("login").addEventListener("click", _send);
	document.getElementById("reset").addEventListener("click", _reset);
    }
    function wsInit() {
	ws = new WebSocket(wsURL);
	infoBox.removeEventListener("click", wsInit);
	var typing = document.getElementById("typing");
	ws.onopen = function () {
	    console.log("WS-OK");
	    infoBox.classList.remove("warn");
	    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() {
	tmsel = document.querySelector("select[name=team]");
	nmsel = document.querySelector("select[name=name]");
	txsel = document.getElementById("text");
	document.getElementById("ranking").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");
	wsInit();
    }
    document.addEventListener("DOMContentLoaded", init, false);
    //loadTeamList("teams.csv");
})();