mirror of https://github.com/JustKato/FreePad.git
261 lines
7.4 KiB
JavaScript
261 lines
7.4 KiB
JavaScript
class PadSocket {
|
|
|
|
/**
|
|
* @type {WebSocket}
|
|
*/
|
|
ws = null;
|
|
/**
|
|
* @type {String}
|
|
*/
|
|
padName = null;
|
|
|
|
/**
|
|
* The actual textarea you write in
|
|
* @type {HTMLTextAreaElement}
|
|
*/
|
|
padContents = null;
|
|
/**
|
|
* The <code> of the preview
|
|
* @type {HTMLElement}
|
|
*/
|
|
padPreview = null;
|
|
|
|
/**
|
|
* Create a new PadSocket
|
|
* @param {string} padName The name of the pad
|
|
* @param {string} connUrl The URL to the websocket
|
|
*/
|
|
constructor(padName, connUrl = null) {
|
|
// Assign the pad name
|
|
this.padName = padName;
|
|
|
|
// Check if a connection URL was mentioned
|
|
if ( connUrl == null ) {
|
|
|
|
let connProtocol = `ws://`;
|
|
if ( window.location.protocol == `https:` ) {
|
|
connProtocol = `wss://`;
|
|
}
|
|
|
|
// Try and connect to the local websocket
|
|
connUrl = connProtocol + window.location.host + `/ws/get/${padName}`;
|
|
}
|
|
|
|
// Connect to the websocket
|
|
const ws = new WebSocket(connUrl);
|
|
|
|
// Bind the onMessage function
|
|
ws.onmessage = this.handleMessage;
|
|
|
|
ws.onopen = () => {
|
|
updateStatus(`Established`, `text-success`);
|
|
}
|
|
|
|
function onFail() {
|
|
updateStatus(`Connection Failed`, `text-dangerous`);
|
|
}
|
|
|
|
// Try and reconnect on failure
|
|
ws.onclose = onFail;
|
|
ws.onerror = onFail;
|
|
|
|
// Assign the websocket
|
|
this.ws = ws;
|
|
|
|
// Get all relevant references from the HTML
|
|
this.padContents = document.getElementById(`pad-content`);
|
|
this.padPreview = document.getElementById(`textarea-preview`);
|
|
}
|
|
|
|
/**
|
|
* @description Send a message to the server
|
|
* @param {string} eventType The type of event, this can be anything really, it's just used for routing by the server
|
|
* @param {Object} message The message to send out to the server, this can only be of format string but JSON is parsed.
|
|
*/
|
|
sendMessage = (eventType, message) => {
|
|
|
|
if ( this.ws.readyState !== WebSocket.OPEN ) {
|
|
throw new Error(`The websocket connection is not active`);
|
|
}
|
|
|
|
// Check if the message is a string
|
|
if ( typeof message !== 'object' ) {
|
|
// Convert the message into a map[string]interface{}
|
|
message = {
|
|
"message": message,
|
|
};
|
|
}
|
|
|
|
// TODO: Compress the message, usually we will be sending the whole body of the pad from the client to the server or vice-versa.
|
|
this.ws.send( JSON.stringify({
|
|
eventType,
|
|
padName: this.padName,
|
|
message,
|
|
}))
|
|
|
|
}
|
|
|
|
/**
|
|
* Handle the message from the socket based on the message type
|
|
* @param {MessageEvent} e The websocket message
|
|
*/
|
|
handleMessage = ev => {
|
|
updateStatus(`Catching Message`, `text-white`);
|
|
|
|
// Check if the message has valid data
|
|
if ( !!ev.data ) {
|
|
// Try and parse the data
|
|
let parsedData = null;
|
|
|
|
try {
|
|
parsedData = JSON.parse(ev.data);
|
|
} catch ( err ) {
|
|
console.error(`Failed to parse the WebSocket data`,err);
|
|
updateStatus(`Parse Fail`, `text-warning`);
|
|
}
|
|
|
|
if ( !!!parsedData['message'] ) {
|
|
console.error(`Failed to find the message`)
|
|
updateStatus(`Message Fail`, `text-warning`);
|
|
return;
|
|
}
|
|
|
|
// Check if this is a pad Content Update
|
|
if ( parsedData['eventType'] === `padUpdate`) {
|
|
// Pass on the parsed data
|
|
this.onPadUpdate(parsedData);
|
|
} // Check if this is a pad Status Update
|
|
else if ( parsedData['eventType'] === `statusUpdate`) {
|
|
// Pass on the parsed data
|
|
this.onStatusUpdate(parsedData);
|
|
}
|
|
|
|
updateStatus(`Established`, `text-success`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whenever a pad update is trigered, run this function
|
|
* @param {Object} The response from the server
|
|
*/
|
|
onPadUpdate = data => {
|
|
// Check that the content is clear
|
|
if ( !!data['message']['content'] ) {
|
|
// Send over the new content to be updated.
|
|
updatePadContent(data['message']['content']);
|
|
}
|
|
}
|
|
|
|
onStatusUpdate = data => {
|
|
// Check that the content is clear
|
|
if ( !!data['message']['currentViewers'] ) {
|
|
// Get the amount of viewers reported by the server
|
|
const viewerCount = Number(data['message']['currentViewers']);
|
|
// Check if this is a valid number
|
|
if ( Number.isNaN(viewerCount) ) {
|
|
// Looks like this is a malformed message
|
|
return console.error(`Malformed Message`, data);
|
|
}
|
|
|
|
// Send over the new content to be updated.
|
|
updatePadViewers(viewerCount);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sending a pad update for each keystroke to the server.
|
|
* @param {String} msg The new contents of the pad
|
|
*/
|
|
sendPadUpdate = msg => {
|
|
// Get the contents of the pad
|
|
const padContents = this.padContents.value;
|
|
|
|
// Send the data over the webSocket
|
|
this.sendMessage(`padUpdate`, {
|
|
"content": padContents,
|
|
});
|
|
|
|
updatePadContent(padContents, false);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Update the contents of the pad
|
|
* @param {String} newContent
|
|
*/
|
|
function updatePadContent(newContent, textArea = true) {
|
|
// Update the textarea
|
|
if ( textArea ) {
|
|
document.getElementById(`pad-content`).value = newContent;
|
|
}
|
|
|
|
// Update the preview
|
|
const prev = document.getElementById(`textarea-preview`);
|
|
const shouldScroll = prev.scrollTop >= (prev.scrollHeight - Number(getComputedStyle(prev).height.replace(/px/g, ''))) * 0.98;
|
|
|
|
prev.innerHTML = escapeHtml(newContent);
|
|
|
|
prev.classList.remove(`language-undefined`);
|
|
|
|
prev.classList.forEach( c => {
|
|
if ( c.indexOf(`language-`) != -1 ) {
|
|
prev.classList.remove(c);
|
|
}
|
|
})
|
|
|
|
try { // highlights
|
|
hljs.highlightElement(document.getElementById(`textarea-preview`));
|
|
} catch ( err ) {
|
|
console.err(err);
|
|
}
|
|
|
|
// Check if we should follow the bottom scrolling
|
|
if (shouldScroll) {
|
|
prev.scrollTop = prev.scrollHeight;
|
|
}
|
|
|
|
}
|
|
|
|
function updatePadViewers(vc) {
|
|
// Get the reference to the viewers count inputElement
|
|
/**
|
|
* @type {HTMLInputElement}
|
|
*/
|
|
const viewerCount = document.getElementById(`currentViewers`);
|
|
|
|
// Get the amount of total viewers
|
|
const totalViews = viewerCount.value.split("|")[1].trim();
|
|
|
|
// Set back the real value
|
|
viewerCount.value = `${vc} | ${totalViews}`;
|
|
}
|
|
|
|
function connectSocket() {
|
|
// Check if the socket is established
|
|
if ( !!!window.socket || window.socket.readyState !== WebSocket.OPEN ) {
|
|
updateStatus(`Connecting...`, `text-warning`);
|
|
// Connect the socket
|
|
window.socket = new PadSocket(padTitle);
|
|
}
|
|
|
|
}
|
|
|
|
// TODO: Test if this is actually necessary or the DOMContentLoaded event would suffice
|
|
// wait for the whole window to load
|
|
window.addEventListener(`load`, e => {
|
|
connectSocket()
|
|
})
|
|
|
|
|
|
// lol
|
|
function escapeHtml(html){
|
|
const text = document.createTextNode(html);
|
|
const p = document.createElement('p');
|
|
|
|
p.appendChild(text);
|
|
const content = p.innerHTML;
|
|
p.remove();
|
|
|
|
return content;
|
|
} |