From 789e5555ab4c44a1ae779eccf6ccf8340602cf22 Mon Sep 17 00:00:00 2001 From: Roland Reichwein Date: Thu, 26 Jan 2023 20:46:30 +0100 Subject: Websockets: Notify other clients of changes --- html/whiteboard.js | 409 ++++++++++++++++------------------------------------- 1 file changed, 121 insertions(+), 288 deletions(-) (limited to 'html/whiteboard.js') diff --git a/html/whiteboard.js b/html/whiteboard.js index 7777d4e..9610468 100644 --- a/html/whiteboard.js +++ b/html/whiteboard.js @@ -3,145 +3,126 @@ function init() { init_board(); } -class AdjustingTimer { - constructor() { - this.update_counter = 0; // counting seconds since last counter reset - } +var revision; - fast_mode() { - if (this.update_counter < 5*60) - return true; - return false; - } +// helper for breaking feedback loop +var caretpos = 0; - // private method - // returns current interval in ms - current_update_interval() - { - if (this.fast_mode()) - return 2000; // 2s - else - return 5 * 60000; // 5min - }; +function showQRWindow() +{ + document.getElementById("qrwindow").style.display = 'block'; +} - // private - count() { - this.update_counter += this.current_update_interval() / 1000; - }; +function hideQRWindow() +{ + document.getElementById("qrwindow").style.display = 'none'; +} - // private - on_timeout() { - this.m_fn(); - this.count(); - var _this = this; - this.update_timer = setTimeout(function(){_this.on_timeout();}, this.current_update_interval()); - }; +var websocket; - // to be called once on startup - start(fn) { - this.m_fn = fn; - var _this = this; - this.update_timer = setTimeout(function(){_this.on_timeout();}, this.current_update_interval()); - }; +// +// Callbacks for websocket data of different types +// - // to be called on activity: - // * changes from remote - // * changes by ourselves - // * local activity, e.g. mouse move, or key presses - reset() { - if (!this.fast_mode()) { - this.update_counter = 0; - clearTimeout(this.update_timer); - var _this = this; - this.update_timer = setTimeout(function(){_this.on_timeout();}, this.current_update_interval()); - } else { - this.update_counter = 0; - } - }; +function on_getfile(data, rev, pos) +{ + var board = document.getElementById("board"); + if (board.value != data) { + board.value = data; + } + textAreaSetPos("board", pos); + revision = rev; } -var timer = new AdjustingTimer(); +function on_newid(id) +{ + var new_location = document.location.origin + document.location.pathname + '?id=' + id; + window.location.href = new_location; +} -function showQRWindow() +function on_qrcode(png) { - document.getElementById("qrwindow").style.display = 'block'; + var blob = new Blob([png], {type: 'image/png'}); + var url = URL.createObjectURL(blob); + var img = document.getElementById("qrcode"); + img.src = url; + showQRWindow(); } -function hideQRWindow() +function on_modify_ack(rev) { - document.getElementById("qrwindow").style.display = 'none'; + revision = rev; } -function init_board() { - var xhr = new XMLHttpRequest(); +function on_message(e) { + var parser = new DOMParser(); + var xmlDocument = parser.parseFromString(e.data, "text/xml"); - const searchParams = (new URL(document.location)).searchParams; - if (!searchParams.has('id')) { - redirect_to_new_page(); - return; - } - - // run on data received back - xhr.onreadystatechange = function() { - if (this.readyState == 3) { - //set_status("Please wait while downloading " + filename + " ..."); - return; - } - if (this.readyState != 4) { - return; - } - if (this.status != 200) { - //set_status("Server Error while retrieving " + filename + ", status: " + this.status + " " + this.statusText); - return; - } - - var file = new Blob([this.response]); - reader = new FileReader(); - reader.onload = function() { - var board = document.getElementById("board"); - var pos = reader.result.indexOf('\x01'); - if (pos == -1) { // not found - board.value = reader.result; - } else { - board.value = reader.result.substr(0, pos) + reader.result.substr(pos + 1); - } - textAreaSetPos("board", pos); - - // Initialization done. Now we can start modifying. - board.addEventListener("input", function() {on_modify(); }); - board.addEventListener("selectionchange", function() {on_modify(); }); - - // Initialization done. Now we can start modifying. - document.addEventListener("mousemove", function() {timer.reset(); }); - - timer.start(checkupdate); - } - - reader.readAsBinaryString(file); - - //set_status(""); // OK + 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("cursorpos")[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 == "error") { + alert(xmlDocument.getElementsByTagName("message")[0].textContent); + } else { + alert("Unhandled message type: " + e.data + "|" + type); } +} - var parser = new DOMParser(); - var xmlDocument = parser.parseFromString("", "text/xml"); - - var requestElement = xmlDocument.getElementsByTagName("request")[0]; +function handleSelection() { + const activeElement = document.activeElement - var commandElement = xmlDocument.createElement("command"); - commandElement.appendChild(document.createTextNode("getfile")); - requestElement.appendChild(commandElement); + if (activeElement && activeElement.id === 'board') { + if (caretpos != activeElement.selectionStart) { + on_selectionchange(activeElement.selectionStart); + caretpos = activeElement.selectionStart; + } + } +} - var idElement = xmlDocument.createElement("id"); - idElement.appendChild(document.createTextNode(get_id())); - requestElement.appendChild(idElement); +function init_board() { + 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; + } - xhr.open("POST", "whiteboard.fcgi", true); - xhr.setRequestHeader("Content-type", "text/xml"); - xhr.responseType = 'blob'; - xhr.send(xmlDocument); + 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(); }); + + websocket.send("getfile" + get_id() + ""); + }; + + websocket.onclose = function(e) { + alert("Server connection closed."); + }; + + websocket.onerror = function(e) { + alert("Error: Server connection closed."); + }; - //set_status("Please wait while server prepares " + filename + " ..."); - document.getElementById("qrwindow").onclick = function() { hideQRWindow(); } @@ -161,81 +142,20 @@ function get_id() return searchParams.get('id'); } +// from html function on_new_page() { - redirect_to_new_page(); + redirect_to_new_page(); } function redirect_to_new_page() { - var xhr = new XMLHttpRequest(); - - // run on data received back - xhr.onreadystatechange = function() { - if (this.readyState == 3) { - //set_status("Please wait while downloading " + filename + " ..."); - return; - } - if (this.readyState != 4) { - return; - } - if (this.status != 200) { - //set_status("Server Error while retrieving " + filename + ", status: " + this.status + " " + this.statusText); - return; - } - - var id = this.responseText; - //alert("location=" + document.location.href); - var new_location = document.location.href; - var pos = new_location.search("\\?"); - if (pos >= 0) - new_location = new_location.substring(0, pos); - new_location += '?id=' + id; - - window.location.href = new_location; - //set_status(""); // OK - } - - 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("newid")); - requestElement.appendChild(commandElement); - - xhr.open("POST", "whiteboard.fcgi", true); - xhr.setRequestHeader("Content-type", "text/xml"); - xhr.send(xmlDocument); - - //set_status("Please wait while server prepares " + filename + " ..."); + websocket.send("newid"); } // local change done -function on_modify() +function on_input() { - timer.reset(); - - var xhr = new XMLHttpRequest(); - - // run on data received back - xhr.onreadystatechange = function() { - if (this.readyState == 3) { - //set_status("Please wait while downloading " + filename + " ..."); - return; - } - if (this.readyState != 4) { - return; - } - if (this.status != 200) { - //set_status("Server Error while retrieving " + filename + ", status: " + this.status + " " + this.statusText); - return; - } - - //set_status(""); // OK - } - var parser = new DOMParser(); var xmlDocument = parser.parseFromString("", "text/xml"); @@ -250,131 +170,47 @@ function on_modify() requestElement.appendChild(idElement); var dataElement = xmlDocument.createElement("data"); - dataElement.appendChild(document.createTextNode(addPos(document.getElementById("board").value, document.getElementById("board").selectionStart))); + dataElement.appendChild(document.createTextNode(document.getElementById("board").value)); requestElement.appendChild(dataElement); - xhr.open("POST", "whiteboard.fcgi", true); - xhr.setRequestHeader("Content-type", "text/xml"); - xhr.responseType = 'blob'; - xhr.send(xmlDocument); - - //set_status("Please wait while server prepares " + filename + " ..."); -} - -// checksum of string -function checksum32(s) { - var result = 0; - for (var i = 0; i < s.length; i++) { - result = ((((result >>> 1) | ((result & 1) << 31)) | 0) ^ (s.charCodeAt(i) & 0xFF)) | 0; - } - return (result & 0x7FFFFFFF) | 0; + websocket.send(new XMLSerializer().serializeToString(xmlDocument)); } -function textAreaSetPos(id, pos) +// for cursor position +function on_selectionchange(pos) { - document.getElementById(id).selectionStart = pos; - document.getElementById(id).selectionEnd = pos; -} - -function addPos(s, pos) -{ - return s.substr(0, pos) + '\x01' + s.substr(pos); -} - -// gets called by regular polling -function checkupdate() { - var xhr = new XMLHttpRequest(); - - // run on data received back - xhr.onreadystatechange = function() { - if (this.readyState == 3) { - //set_status("Please wait while downloading " + filename + " ..."); - return; - } - if (this.readyState != 4) { - return; - } - if (this.status != 200) { - //set_status("Server Error while retrieving " + filename + ", status: " + this.status + " " + this.statusText); - return; - } - - // no change if response is text/plain - if (this.getResponseHeader("Content-Type") == "application/octet-stream") { - timer.reset(); - var file = new Blob([this.response]); - reader = new FileReader(); - reader.onload = function() { - var board = document.getElementById("board"); - var pos = reader.result.indexOf('\x01'); - if (pos == -1) { // not found - board.value = reader.result; - } else { - board.value = reader.result.substr(0, pos) + reader.result.substr(pos + 1); - } - textAreaSetPos("board", pos); - } - - reader.readAsBinaryString(file); - } - - //set_status(""); // OK - } - 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("checkupdate")); + commandElement.appendChild(document.createTextNode("cursorpos")); requestElement.appendChild(commandElement); var idElement = xmlDocument.createElement("id"); idElement.appendChild(document.createTextNode(get_id())); requestElement.appendChild(idElement); - var checksumElement = xmlDocument.createElement("checksum"); - checksumElement.appendChild(document.createTextNode(checksum32(addPos(document.getElementById("board").value, document.getElementById("board").selectionStart)))); - requestElement.appendChild(checksumElement); - - xhr.open("POST", "whiteboard.fcgi", true); - xhr.setRequestHeader("Content-type", "text/xml"); - xhr.responseType = 'blob'; - xhr.send(xmlDocument); + var dataElement = xmlDocument.createElement("pos"); + dataElement.appendChild(document.createTextNode(pos)); + requestElement.appendChild(dataElement); - //set_status("Please wait while server prepares " + filename + " ..."); + websocket.send(new XMLSerializer().serializeToString(xmlDocument)); } -function on_qrcode() +function textAreaSetPos(id, pos) { - var xhr = new XMLHttpRequest(); - - // run on data received back - xhr.onreadystatechange = function() { - if (this.readyState == 3) { - //set_status("Please wait while downloading " + filename + " ..."); - return; - } - if (this.readyState != 4) { - return; - } - if (this.status != 200) { - //set_status("Server Error while retrieving " + filename + ", status: " + this.status + " " + this.statusText); - return; - } - - if (this.getResponseHeader("Content-Type") == "image/png") { - var blob = new Blob([this.response], {type: 'image/png'}); - var url = URL.createObjectURL(blob); - var img = document.getElementById("qrcode"); - img.src = url; - showQRWindow(); - } - - //set_status(""); // OK + if (document.getElementById(id).selectionStart != pos) { + document.getElementById(id).selectionStart = pos; + document.getElementById(id).selectionEnd = pos; + caretpos = pos; } +} +// HTML button +function on_qrcode() +{ var parser = new DOMParser(); var xmlDocument = parser.parseFromString("", "text/xml"); @@ -388,9 +224,6 @@ function on_qrcode() idElement.appendChild(document.createTextNode(document.location)); requestElement.appendChild(idElement); - xhr.open("POST", "whiteboard.fcgi", true); - xhr.setRequestHeader("Content-type", "text/xml"); - xhr.responseType = 'blob'; - xhr.send(xmlDocument); + websocket.send(new XMLSerializer().serializeToString(xmlDocument)); } -- cgit v1.2.3