// started on main page load function init() { init_board(); } var revision; var modify_in_progress = 0; var baseline = ""; // data contents relating to revision, acknowledged by server var baseline_candidate = ""; // will become baseline, after ack by server // helper for breaking feedback loop var caretpos = 0; function set_status(message) { if (message == "") { document.getElementById("status").textContent = message; document.getElementById("status").style.display = 'inline'; } else { document.getElementById("status").textContent = ""; document.getElementById("status").style.display = 'none'; } } function showQRWindow() { document.getElementById("qrwindow").style.display = 'block'; } function hideQRWindow() { document.getElementById("qrwindow").style.display = 'none'; } var websocket; // // Callbacks for websocket data of different types // function on_getfile(data, rev, pos) { var board = document.getElementById("board"); if (board.value != data) { board.value = data; } revision = rev; baseline = data; textAreaSetPos("board", pos); } function on_getdiff(diff, rev, pos) { if (rev != revision + 1) console.log("Revision skipped on diff receive: " + rev + " after " + revision); var board = document.getElementById("board"); var old_version_ptr = allocateUTF8(board.value); var diff_ptr = allocateUTF8(new XMLSerializer().serializeToString(diff)); var new_version_ptr = Module._diff_apply(old_version_ptr, diff_ptr); var data = UTF8ToString(new_version_ptr); board.value = data; _free(old_version_ptr); _free(new_version_ptr); _free(diff_ptr); revision = rev; baseline = data; textAreaSetPos("board", pos); } function on_getpos(pos) { textAreaSetPos("board", pos); } function on_newid(id) { var new_location = document.location.origin + document.location.pathname + '?id=' + id; window.location.href = new_location; } function on_qrcode(png) { var url = "data:image/png;base64," + png; var img = document.getElementById("qrcode"); img.src = url; showQRWindow(); } function on_version(version) { document.getElementById("version").textContent = version; } function on_pdf(pdf) { var a = document.getElementById("download-a"); a.href = "data:application/pdf;base64," + pdf; a.download = get_id() + ".pdf" a.click(); } function on_modify_ack(rev) { if (rev != revision + 1) console.log("Revision skipped on published local change: " + rev + " after " + revision); revision = rev; baseline = baseline_candidate; modify_in_progress = 0; } function on_message(e) { var parser = new DOMParser(); var xmlDocument = parser.parseFromString(e.data, "text/xml"); var type = xmlDocument.getElementsByTagName("type")[0].textContent; if (type == "getfile") { on_getfile(xmlDocument.getElementsByTagName("data")[0].textContent, parseInt(xmlDocument.getElementsByTagName("revision")[0].textContent), parseInt(xmlDocument.getElementsByTagName("pos")[0].textContent)); } else if (type == "getdiff") { on_getdiff(xmlDocument.getElementsByTagName("diff")[0], parseInt(xmlDocument.getElementsByTagName("revision")[0].textContent), parseInt(xmlDocument.getElementsByTagName("pos")[0].textContent)); } else if (type == "getpos") { on_getpos(parseInt(xmlDocument.getElementsByTagName("pos")[0].textContent)); } else if (type == "modify") { on_modify_ack(parseInt(xmlDocument.getElementsByTagName("revision")[0].textContent)); } else if (type == "newid") { on_newid(xmlDocument.getElementsByTagName("id")[0].textContent); } else if (type == "qrcode") { on_qrcode(xmlDocument.getElementsByTagName("png")[0].textContent); } else if (type == "version") { on_version(xmlDocument.getElementsByTagName("version")[0].textContent); } else if (type == "pdf") { on_pdf(xmlDocument.getElementsByTagName("pdf")[0].textContent); } else if (type == "error") { alert(xmlDocument.getElementsByTagName("message")[0].textContent); } else { alert("Unhandled message type: " + e.data + "|" + type); } } function handleSelection() { const activeElement = document.activeElement if (activeElement && activeElement.id === 'board') { if (caretpos != activeElement.selectionStart) { on_selectionchange(activeElement.selectionStart); caretpos = activeElement.selectionStart; } } } function connect_websocket() { document.getElementById("reconnect").style.display = 'none'; set_status("Connecting..."); var newlocation = location.origin + location.pathname; newlocation = newlocation.replace(/^http/, 'ws'); if (newlocation.slice(-1) != "/") newlocation += "/"; newlocation += "websocket"; websocket = new WebSocket(newlocation); websocket.onmessage = function(e) { on_message(e); }; websocket.onopen = function(e) { const searchParams = (new URL(document.location)).searchParams; if (!searchParams.has('id')) { redirect_to_new_page(); return; } websocket.send("getversion"); websocket.send("getfile" + get_id() + ""); set_status(""); // ok document.getElementById("board").focus(); }; websocket.onclose = function(e) { alert("Server connection closed."); document.getElementById("reconnect").style.display = 'inline'; document.getElementById("reconnect").focus(); }; websocket.onerror = function(e) { alert("Error: Server connection closed."); document.getElementById("reconnect").style.display = 'inline'; document.getElementById("reconnect").focus(); }; } // button in html function on_reconnect_click() { connect_websocket(); } function init_board() { set_status("Loading..."); Module.onRuntimeInitialized = () => { connect_websocket(); }; var board = document.getElementById("board"); board.addEventListener("input", function() {on_input(); }); // Need this workaround (different from direct on_selectionchange) for Chrome. // Otherwise, callback will not be called on Chrome. document.addEventListener("selectionchange", handleSelection); //board.addEventListener("selectionchange", function() {on_selectionchange(); }); document.getElementById("qrwindow").onclick = function() { hideQRWindow(); } document.onkeydown = function(evt) { if (evt.key == "Escape") { hideQRWindow(); } } document.getElementById("board").focus(); } function get_id() { const searchParams = (new URL(document.location)).searchParams; return searchParams.get('id'); } // from html function on_new_page() { redirect_to_new_page(); } function redirect_to_new_page() { websocket.send("newid"); } // local change done function on_input() { if (modify_in_progress == 1) { console.log("Deferring on_input handler by 100ms"); setTimeout(function(){on_input();}, 100); // re-try after 100ms return; } modify_in_progress = 1; var parser = new DOMParser(); var xmlDocument = parser.parseFromString("", "text/xml"); var requestElement = xmlDocument.getElementsByTagName("request")[0]; var commandElement = xmlDocument.createElement("command"); commandElement.appendChild(document.createTextNode("modify")); requestElement.appendChild(commandElement); var idElement = xmlDocument.createElement("id"); idElement.appendChild(document.createTextNode(get_id())); requestElement.appendChild(idElement); baseline_candidate = document.getElementById("board").value; if (baseline == baseline_candidate) { modify_in_progress = 0; return; } var revisionElement = xmlDocument.createElement("baserev"); revisionElement.appendChild(document.createTextNode(revision)); requestElement.appendChild(revisionElement); var old_version = allocateUTF8(baseline); var new_version = allocateUTF8(baseline_candidate); var diff = Module._diff_create(old_version, new_version); var diffDocument = parser.parseFromString(UTF8ToString(diff), "text/xml"); _free(old_version); _free(new_version); _free(diff); requestElement.appendChild(xmlDocument.importNode(diffDocument.getElementsByTagName("diff")[0], true)); var posElement = xmlDocument.createElement("pos"); posElement.appendChild(document.createTextNode(document.getElementById("board").selectionStart)); requestElement.appendChild(posElement); websocket.send(new XMLSerializer().serializeToString(xmlDocument)); } // for cursor position function on_selectionchange(pos) { var parser = new DOMParser(); var xmlDocument = parser.parseFromString("", "text/xml"); var requestElement = xmlDocument.getElementsByTagName("request")[0]; var commandElement = xmlDocument.createElement("command"); commandElement.appendChild(document.createTextNode("cursorpos")); requestElement.appendChild(commandElement); var idElement = xmlDocument.createElement("id"); idElement.appendChild(document.createTextNode(get_id())); requestElement.appendChild(idElement); var posElement = xmlDocument.createElement("pos"); posElement.appendChild(document.createTextNode(pos)); requestElement.appendChild(posElement); websocket.send(new XMLSerializer().serializeToString(xmlDocument)); } function textAreaSetPos(id, pos) { if (document.getElementById(id).selectionStart != pos) { document.getElementById(id).selectionStart = pos; document.getElementById(id).selectionEnd = pos; caretpos = pos; } } // HTML button function on_qrcode_click() { var parser = new DOMParser(); var xmlDocument = parser.parseFromString("", "text/xml"); var requestElement = xmlDocument.getElementsByTagName("request")[0]; var commandElement = xmlDocument.createElement("command"); commandElement.appendChild(document.createTextNode("qrcode")); requestElement.appendChild(commandElement); var idElement = xmlDocument.createElement("url"); idElement.appendChild(document.createTextNode(document.location)); requestElement.appendChild(idElement); websocket.send(new XMLSerializer().serializeToString(xmlDocument)); } function on_pdf_click() { websocket.send("pdf" + get_id() + ""); }