I'm working on rewriting the Cardshifter HTML Client with vanilla JavaScript, the original HTML Client is written with Angular but I really wanted to stay away from anything npm and just go back to basics. Note that I have no desire to use libraries like jQuery, Underscore, etc. unless absolutely necessary.
This is the landing page where a user selects a server to connect to and login. I would like feedback on any and all facets of the code, especially if I'm using anti-patterns that I could avoid using throughout the rest of the client. All the sections are documented so I won't spend time explaining what everything does here.
If you want to skim over the trivial things, the primary code files I would like feedback on are:
sections/login/login.js
server_interface/server_interface.js
utils/loadHtml.js
Here is an animated GIF that shows some of the functionality, namely checking whether a given server can offer a valid WebSocket connection. (note that the alert
showing the user name has since been removed, it was for debugging purposes.
Directory structure
Here is how my files are structured at the moment. I have a few other directories for images and such that I excluded because they are not used yet.
index.html
global.js
sections/
login/
login.html
login.js
top_navbar/
top_navbar.html
server_interface/
server_interface.js
styles/
cardshifter.css
utils/
formatDate.js
loadHtml.js
logDebugMessage.js
Code
index.html
<!DOCTYPE html>
<html>
<head>
<title>Cardshifter</title>
<!-- Bootstrap -->
<link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.0/css/bootstrap.min.css" rel="stylesheet" />
<!-- Local styles -->
<link rel="stylesheet" href="styles/cardshifter.css" />
<!-- Local JavaScript -->
<script src="global.js"></script>
<script src="server_interface/server_interface.js"></script>
<script src="utils/loadHtml.js"></script>
<script src="utils/formatDate.js"></script>
<script src="utils/logDebugMessage.js"></script>
<!-- Local Section JavaScript -->
<script src="sections/login/login.js"></script>
<!-- Favicon links -->
<link rel="apple-touch-icon" sizes="57x57" href="images/favicon/apple-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="60x60" href="images/favicon/apple-icon-60x60.png" />
<link rel="apple-touch-icon" sizes="72x72" href="images/favicon/apple-icon-72x72.png" />
<link rel="apple-touch-icon" sizes="76x76" href="images/favicon/apple-icon-76x76.png" />
<link rel="apple-touch-icon" sizes="114x114" href="images/favicon/apple-icon-114x114.png" />
<link rel="apple-touch-icon" sizes="120x120" href="images/favicon/apple-icon-120x120.png" />
<link rel="apple-touch-icon" sizes="144x144" href="images/favicon/apple-icon-144x144.png" />
<link rel="apple-touch-icon" sizes="152x152" href="images/favicon/apple-icon-152x152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="images/favicon/apple-icon-180x180.png" />
<link rel="icon" type="image/png" sizes="192x192" href="images/favicon/android-icon-192x192.png" />
<link rel="icon" type="image/png" sizes="32x32" href="images/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="96x96" href="images/favicon/favicon-96x96.png" />
<link rel="icon" type="image/png" sizes="16x16" href="images/favicon/favicon-16x16.png" />
<link rel="manifest" href="images/favicon/manifest.json" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="msapplication-TileImage" content="images/favicon/ms-icon-144x144.png" />
<meta name="theme-color" content="#ffffff" />
</head>
<body>
<!-- Top navigation bar -->
<div id="top_navbar_container">
<script>
const navbarContainerId = "top_navbar_container";
const navbarFilePath = "sections/top_navbar/top_navbar.html";
loadHtml(navbarContainerId, navbarFilePath)
.then(function() {
if (DEBUG) {
logDebugMessage(`"${navbarFilePath}" loaded OK!`);
}
});
</script>
</div>
<div class="csh-body">
<div id="login_container">
<script>
const loginContainerId = "login_container";
const loginFilePath = "sections/login/login.html";
loadHtml(loginContainerId, loginFilePath)
.then(function() {
loginHandler();
if (DEBUG) {
logDebugMessage(`"${loginFilePath}" loaded OK!`);
}
});
</script>
</div>
</div>
</body>
</html>
global.js
/*
* This file is for global values to be used throughout the site.
*/
'use strict';
/*
* Setting to `true` will log messages in the browser console
* to help in debugging and keeping track of what is happening on the page.
* This should be set to `false` on the public client.
*/
const DEBUG = true;
/*
* Port number used for WebSocket.
*/
const WS_PORT = 4243;
/*
* List of game server names and WebSocket URIs.
*/
const GAME_SERVERS = {
"localhost" : `ws://127.0.0.1:${WS_PORT}`,
"dwarftowers.com" : `ws://dwarftowers.com:${WS_PORT}`,
"zomis.net" : `ws://stats.zomis.net:${WS_PORT}`,
"Other" : ""
};
/**
* Default date format for the application.
* @type String
*/
const DEFAULT_DATE_FORMAT = "yyyy/MM/dd hh:mm:ss";
sections
sections/login/login.html
<div id="login">
<h4>Please log in to continue.</h4>
<form name="login_form" id="login_form" class="login-form">
<div id="login_server_select_container" class="form-group">
<label for="login_server_list" aria-label="Server">Server:</label>
<select name="login_server_list" id="login_server_list" class="form-control">
</select>
<div id="login_server_other_container" class="form-group" style="display : none">
<label for="login_server_other_input">Other Server:</label>
<input name="login_server_other_input" id="login_server_other_input" type="text" class="form-control" />
<input type="button" name="test_login_server_other" id="test_login_server_other" class="btn" value="Test connection" />
</div>
<input readonly name="server_loading_display" id="server_connecting" class="form-control" style="background-color: #DDD; display: none" />
<label for="login_secure">Is secure server:</label>
<input name="login_secure" id="login_secure" type="checkbox" value="secure" />
<span id="login_server_connection_status" class="label" style="display: block; text-align: left"></span>
</div>
<div id="login_username_container" class="form-group">
<label for="login_username">Username:</label>
<input name="login_username" id="login_username" type="text" class="form-control" placeholder="Enter name..." />
</div>
<div class="form-group">
<input type="button" name="login_submit" id="login_submit" type="button" class="btn btn-success" value="Log in" />
</div>
</form>
</div>
sections/login/login.js
/* global GAME_SERVERS, DEBUG, CardshifterServerAPI, DEFAULT_DATE_FORMAT */
const loginHandler = function() {
const serverSelectContainer = document.getElementById("login_server_select_container");
const serverSelect = serverSelectContainer.querySelector("#login_server_list");
const serverOtherInputContainer = serverSelectContainer.querySelector("#login_server_other_container");
const serverLoading = serverSelectContainer.querySelector("#server_connecting");
const connStatusMsg = serverSelectContainer.querySelector("#login_server_connection_status");
let currentServerHasValidConnection = null;
/**
* Adds options to the server selection based on GAME_SERVERS global.
* @returns {undefined}
*/
const populateServerSelect = function() {
for (let key in GAME_SERVERS) {
if (GAME_SERVERS.hasOwnProperty(key)) {
const option = document.createElement("option");
option.text = key;
option.value = GAME_SERVERS[key];
serverSelect.add(option);
}
}
};
/**
* Tests the WebSocket connection to a server and displays a message on the page
* to give the user information about the connection status.
* @returns {undefined}
*/
const testWebsocketConnection = function() {
const serverUri = serverSelect.value;
const isSecure = false;
let msgText = "";
if (serverUri) {
displayConnStatus("connecting", serverUri);
/**
* Test WebSocket connection and display status if successful.
* @returns {undefined}
*/
const onReady = function() {
makeServerSelectReadWrite();
msgText = displayConnStatus("success", serverUri);
if (DEBUG) { logDebugMessage(msgText); }
currentServerHasValidConnection = true;
};
/**
* Test WebSocket connection and display status if failed.
* @returns {undefined}
*/
const onError = function() {
makeServerSelectReadWrite();
msgText = displayConnStatus("failure", serverUri);
if (DEBUG) { logDebugMessage(msgText); }
currentServerHasValidConnection = false;
};
CardshifterServerAPI.init(serverUri, isSecure, onReady, onError);
makeServerSelectReadOnly(serverUri);
}
else {
displayConnStatus("unknown", serverUri);
}
};
/**
* Displays connection status in the page.
* @param {string} status - Keyword representing the connection status
* @param {type} serverUri - The URI of the server the client is connecting to
* @returns {String} - The message text, largely for debug purposes
*/
const displayConnStatus = function(status, serverUri) {
let msgText = "";
switch (status.toLowerCase()) {
case "connecting":
msgText =
`<h5>Connecting to server...</h5>` +
`<pre class='bg-warning'>` +
`Address: ${serverUri}` +
`\n${formatDate(new Date())}` +
`</pre>`;
connStatusMsg.className = "label label-warning";
connStatusMsg.innerHTML = msgText;
break;
case "success":
msgText =
`<h5>WebSocket connection OK.</h5>\n` +
`<pre class='bg-success'>`+
`Address: ${serverUri}` +
`\n${formatDate(new Date())}` +
`</pre>`;
connStatusMsg.innerHTML = msgText;
connStatusMsg.className = "label label-success";
break;
case "failure":
msgText =
`<h5>WebSocket connection FAILED.</h5>\n` +
`<pre class='bg-danger'>`+
`Address: ${serverUri}` +
`\n${formatDate(new Date())}` +
`</pre>`;
connStatusMsg.innerHTML = msgText;
connStatusMsg.className = "label label-danger";
break;
case "unknown":
default:
msgText = `<h5>Unknown connection status...</h5>`;
connStatusMsg.innerHTML = msgText;
connStatusMsg.className = "label label-default";
break;
}
return msgText;
};
/**
* Hides the `select` element and shows a read-only `input` instead.
* @param {string} serverUri
* @returns {undefined}
*/
const makeServerSelectReadOnly = function(serverUri) {
const selector = document.getElementById("login_server_list");
const connecting = document.getElementById("server_connecting");
selector.style.display = "none";
connecting.style.display = "block";
connecting.value = `Connecting to ${serverUri}...`;
};
/**
* Makes the server `select` element visible and hides the read-only `input`
* @returns {undefined}
*/
const makeServerSelectReadWrite = function() {
const selector = document.getElementById("login_server_list");
const connecting = document.getElementById("server_connecting");
selector.style.display = "block";
connecting.style.display = "none";
};
/**
* Displays an input field for server address if "Other" server is selected.
* @returns {undefined}
*/
const handleServerSelectChanges = function() {
if (serverSelect.value) {
serverOtherInputContainer.style.display = "none";
}
else {
serverOtherInputContainer.style.display = "block";
}
};
/**
* Attempts to login to game server.
* @returns {undefined}
*/
const tryLogin = function() {
const username = document.getElementById("login_username").value;
if (!username) {
displayNoUsernameWarning();
}
else {
const isSecure = false;
var loggedIn = null;
let serverUri = serverSelect.value;
if (!serverUri) {
serverUri = document.getElementById("login_server_other_input").value;
}
/**
* Short-circuit login attempt if we've already found that the connection not valid.
* @type String
*/
if (!currentServerHasValidConnection) {
const msg = "Websocket error(error 1)";
console.log(msg);
displayLoginFailureWarning(msg);
}
/**
* Attempt to log in once the WebSocket connection is ready.
* @returns {undefined}
*/
const onReady = function() {
let login = new CardshifterServerAPI.messageTypes.LoginMessage(username);
/**
* Listens for a welcome message from the game server, and stores user values in the browser.
* @param {Object} welcome
* @returns {undefined}
*/
const messageListener = function(welcome) {
const SUCCESS = 200;
const SUCCESS_MESSAGE = "OK";
if(welcome.status === SUCCESS && welcome.message === SUCCESS_MESSAGE) {
localStorage.setItem("username", username);
localStorage.setItem("id", welcome.userId);
localStorage.setItem("playerIndex", null);
localStorage.setItem("game", { "id" : null, "mod" : null });
}
else {
console.log(`${new Date()} server message: ${welcome.message}`);
loggedIn = false;
}
};
try {
CardshifterServerAPI.setMessageListener(messageListener, ["loginresponse"]);
CardshifterServerAPI.sendMessage(login);
}
catch(error) {
const msg = "LoginMessage error(error 2)";
if (DEBUG) { logDebugMessage(`${msg} ${error}`); }
displayLoginFailureWarning(msg, error);
loggedIn = false;
}
};
/**
* Log error if the connection fails
* @returns {undefined}
*/
const onError = function() {
const msg = "Websocket error(error 1)";
if (DEBUG) { logDebugMessage(msg); }
displayLoginFailureWarning(msg);
loggedIn = false;
};
CardshifterServerAPI.init(serverUri, isSecure, onReady, onError);
}
};
/**
* Displays a warning if no username is entered.
* @returns {undefined}
*/
const displayNoUsernameWarning = function() {
const container = document.getElementById("login_username_container");
if (!container.querySelector("#login_username_missing_msg")) {
const msg = document.createElement("span");
msg.id = "login_username_missing_msg";
msg.className = "label label-danger";
msg.innerHTML = "Please enter a username.";
container.appendChild(msg);
}
};
const displayLoginFailureWarning = function(message, error) {
const container = document.getElementById("login_username_container");
const warning = document.createElement("span");
warning.id = "login_failure_msg";
warning.className = "label label-danger";
warning.style = "display: block; text-align: left;";
warning.innerHTML = `<h5>Login failed: ${message}</h5>`;
if (error) {
warning.innerHTML += `<pre>${error}</pre>`;
}
container.appendChild(warning);
};
const testOtherServerConnection = function() {
const otherServerInput = document.getElementById("login_server_other_input");
const otherServerUri = otherServerInput.value;
const isSecure = false;
/**
* Test WebSocket connection and display status if successful.
* @returns {undefined}
*/
const onReady = function() {
makeServerSelectReadWrite();
msgText = displayConnStatus("success", otherServerUri);
if (DEBUG) { logDebugMessage(msgText); }
currentServerHasValidConnection = true;
};
/**
* Test WebSocket connection and display status if failed.
* @returns {undefined}
*/
const onError = function() {
makeServerSelectReadWrite();
msgText = displayConnStatus("failure", otherServerUri);
if (DEBUG) { logDebugMessage(msgText); }
currentServerHasValidConnection = false;
};
CardshifterServerAPI.init(otherServerUri, isSecure, onReady, onError);
makeServerSelectReadOnly();
displayConnStatus("connecting", otherServerUri);
};
/**
* IIFE to setup the login handling for the page it is loaded in.
* @type undefined
*/
const runLoginHandler = function() {
populateServerSelect();
document.getElementById("login_server_list").addEventListener("change", handleServerSelectChanges, false);
document.getElementById("login_server_list").addEventListener("change", testWebsocketConnection, false);
document.getElementById("login_submit").addEventListener("click", tryLogin, false);
document.getElementById("test_login_server_other").addEventListener("click", testOtherServerConnection, false);
testWebsocketConnection();
}();
};
sections/top_navbar/top_navbar.html
<nav id="top_navbar" class="navbar navbar-inverse">
<div class="container-fluid">
<div class="navbar-header">
<!-- TODO fix this logic -->
<div class="navbar-brand csh-top-link">Cardshifter</div>
</div>
<form class="navbar-form">
<ul class ="navbar-form navbar-left" style="margin-top: 8px;">
<li class="dropdown">
<a href="#" class="dropdown-toggle csh-dropdown-link" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
Mods
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li class="cyborg-font">Cyborg Chronicles</li>
<li class="cyborg-font"><a href=#>Game rules</a></li>
<li class="cyborg-font"><a href=#>Cards</a></li>
<li role="separator" class="divider"></li>
<li class="mythos-font">Mythos</li>
<li class="mythos-font"><a href=#>Game rules</a></li>
<li class="mythos-font"><a href=#>Cards</a></li>
</ul>
</li>
</ul>
<ul class ="navbar-form navbar-left" style="margin-top: 8px;">
<li class="dropdown">
<a href="#" class="dropdown-toggle csh-dropdown-link" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
Help
<span class="caret"></span></a>
</li>
</ul>
<ul class ="navbar-form navbar-left" style="margin-top: 8px;">
<li class="dropdown">
<a href="#" class="dropdown-toggle csh-dropdown-link" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
About
<span class="caret"></span></a>
</li>
</ul>
<div class="form-group navbar-form navbar-left">
<input name="disconnect_websocket" id="disconnect_websocket" type="button" value="Log Out" class="btn btn-navbar csh-button" />
</div>
<div class="form-group navbar-form navbar-left">
<input name="display_console" id="display_console" type="button" value="Console" class="btn btn-navbar csh-button" />
</div>
</form>
</div>
</nav>
server_interface
server_interface/server_interface.js
"use strict";
// checks if the string begins with either ws:// or wss://
const wsProtocolFinder = /ws(s)*:\/\//;
/*
* Enum for WebSocket ready state constants.
* @enum {number}
*/
const readyStates = {
CONNECTING : 0,
OPEN : 1,
CLOSING : 2,
CLOSED : 3
};
const MAIN_LOBBY = 1;
let eventTypes = [];
/**
* The base class Message for all the other message types
* to inherit from.
*
* TODO: Would it just be easier to set the `.command` property
* individually for each card type?
*
* @param {string} command - The command of the message.
*/
const Message = function(command) {
this.command = command;
};
/**
* The exception that is thrown when the code is trying to
* interact with the API when the API has not been
* initialized with `.init` yet.
*
* @param {string} message - Informational message about the exception.
*/
const NotInitializedException = function(message) {
this.name = "NotInitializedException";
this.message = message || "";
};
/**
* The exception that is thrown when the code is telling the
* API to interact with the socket when the socket is not
* ready to accept any information.
*
* @param {string} message - Informational message about the exception.
* @param {number} readyState - Ready state constant from WebSocket API, https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
*/
const SocketNotReadyException = function(message, readyState) {
this.name = "SocketNotReadyException";
this.message = message || "";
this.readyState = readyState;
};
/*
* Returns all the keys of an object and its inherited keys.
* This is used so `JSON.stringify` can get the `.command` of a message.
*
* @param {Object} obj - The object to flatten
* @return {Object} - a new Object, containing obj's keys and inherited keys
* @source http://stackoverflow.com/questions/8779249/how-to-stringify-inherited-objects-to-json
*/
const flatten = function(obj) {
let result = Object.create(obj);
for(let key in result) {
// TODO this assignment is weird, why is `result[key]` being assigned to its own value?
result[key] = result[key];
}
return result;
};
/*
* Singleton object to handle communication via WebSocket between the client
* and the game server.
*/
const CardshifterServerAPI = {
socket: null,
messageTypes: {
/*
* Incoming login message.
* A login message from a client to add a user to the available users on the server.
* This login message is required before any other action or message can be performed between a client and a server.
* @constructor
* @param {string} username - The incoming user name passed from client to server, not null
* @example Message: <code>{ "command":"login","username":"JohnDoe" }</code>
*/
LoginMessage : function(username) {
this.username = username;
},
/*
* Request available targets for a specific action to be performed by an entity.
* These in-game messages request a list of all available targets for a given action and entity.
* The client uses this request in order to point out targets (hopefully with a visual aid such as highlighting targets)
* that an entity (such as a creature card, or a player) can perform an action on (for example attack or enchant a card).
* @constructor
* @param {number} gameId - The Id of this game currently being played
* @param {number} id - The Id of this entity which requests to perform an action
* @param {string} action - The name of this action requested to be performed
*/
RequestTargetsMessage : function(gameId, id, action) {
this.gameId = gameId;
this.id = id;
this.action = action;
},
/*
* Make a specific type of request to the server.
* This is used to request an action from the server which requires server-side information.
* @constructor
* @param {string} request - This request
* @param {string} message - The message accompanying this request
*/
ServerQueryMessage : function(request, message) {
this.request = request;
this.message = message;
this.toString = function() {
return `ServerQueryMessage: Request${this.request} message: ${this.message}`;
};
},
/*
* Request to start a new game.
* This is sent from the Client to the Server when this player invites another player (including AI)
* to start a new game of a chosen type.
* @constructor
* @param opponent - The Id of the player entity being invited by this player
* @param gameType - The type / mod of the game chosen by this player
*/
StartGameRequest : function(opponent, gameType) {
this.opponent = opponent;
this.gameType = gameType;
},
/*
* Serialize message from JSON to byte.
* Primarily used for libGDX client.
* Constructor.
* @param type - This message type
*/
TransformerMessage : function(type) {
this.type = type;
},
/*
* Message for a game entity to use a certain ability.
* Game entities (e.g., cards, players) may have one or more ability actions that they can perform.
* Certain abilities can have multiple targets, hence the use of an array.
* @constructor
* Used for multiple target actions.
*
* @param gameId - This current game
* @param entity - This game entity performing an action
* @param action - This action
* @param targets - The set of multiple targets affected by this action
*/
UseAbilityMessage : function(gameId, id, action, targets) {
this.gameId = gameId;
this.id = id;
this.action = action;
this.targets = targets;
this.toString = function() {
return ``
+ `UseAbilityMessage`
+ `[id=${this.id},`
+ `action=${this.action},`
+ `gameId=${this.gameId}`
+ `targets=${this.targets.toString()}]`
;
};
},
/*
* Chat message in game lobby.
* These are messages printed to the game lobby which are visible to all users present at the time the message is posted.
* @constructor
* @param {string} message - The content of this chat message
*/
ChatMessage : function(message) {
this.chatId = MAIN_LOBBY;
this.message = message;
this.toString = function() {
// TODO where does that `from` param/var come from?
return `ChatMessage [chatId=${chatId}, message=${message}, from=${from}]`;
};
},
/*
* Request to invite a player to start a new game.
* @constructor
* @param id - The Id of this invite request
* @param {string} name - The name of the player being invited
* @param gameType - The game type of this invite request
*/
InviteRequest : function(id, name, gameType) {
this.id = id;
this.name = name;
this.gameType = gameType;
},
/*
* Response to an InviteRequest message.
* @constructor
* @param inviteId - Id of this incoming InviteRequest message
* @param {boolean} accepted - Whether or not the InviteRequest is accepted
*/
InviteResponse : function(inviteId, accepted) {
this.inviteId = inviteId;
this.accepted = accepted;
},
/*
* Player configuration for a given game.
* @constructor
* @param gameId - This game
* @param {string} modName - The mod name for this game
* @param {Map} configs - Map of player name and applicable player configuration
*/
PlayerConfigMessage : function(gameId, modName, configs) {
this.gameId = gameId;
this.modName = modName;
this.configs = configs;
this.toString = function() {
return ``
+ `PlayerConfigMessage{`
+ `configs=${configs}, `
+ `gameId=${gameId}, `
+ `modName='${modName}'`
+ `}`
;
};
}
},
/*
* Initializes the API for use.
*
* This sets up all the message types to inherit the main `Message` class, and sets
* up the websocket that will be used to communicate to the server, and to recieve
* information from the server.
*
* @param {string} server - The server address to connect to
* @param {boolean} isSecure - Whether to use SSL for the connection (NOT IMPLEMENTED)
* @param onReady - Function to assign to `socket.onopen`
* @param onError - Function to assign to `socket.onerror`
*/
init : function(server, isSecure, onReady, onError) {
let types = this.messageTypes;
// TODO find out why this unused variable is here
let self = this; // for the events
types.LoginMessage.prototype = new Message("login");
types.RequestTargetsMessage.prototype = new Message("requestTargets");
types.ServerQueryMessage.prototype = new Message("query");
types.StartGameRequest.prototype = new Message("startgame");
types.TransformerMessage.prototype = new Message("serial");
types.UseAbilityMessage.prototype = new Message("use");
types.ChatMessage.prototype = new Message("chat");
types.InviteRequest.prototype = new Message("inviteRequest");
types.InviteResponse.prototype = new Message("inviteResponse");
types.PlayerConfigMessage.prototype = new Message("playerconfig");
NotInitializedException.prototype = new Error();
SocketNotReadyException.prototype = new Error();
// secure websocket is wss://, rather than ws://
const secureAddon = (isSecure ? "s" : "");
// if the protocol is not found in the string, store the correct protocol (is secure?)
const protocolAddon = (wsProtocolFinder.test(server) ? "" : `ws${secureAddon}://`);
let socket = new WebSocket(protocolAddon + server);
socket.onopen = onReady;
socket.onerror = function() {
onError();
this.socket = null;
};
this.socket = socket;
},
/**
* Sends a message to the server
*
* @param {Object} message - The message to send
* @error SocketNotReadyException - The socket is not ready to be used
* @error NotInitializedException - The API has not yet been initialized
*/
sendMessage : function(message) {
const socket = this.socket;
// TODO find out why this unused variable is here
let self = this;
if (socket) {
if (socket.readyState === readyStates.OPEN) {
this.socket.send(JSON.stringify(flatten(message)));
}
else {
throw new SocketNotReadyException("The Websocket is not ready to be used.", socket.readyState);
}
}
else {
throw new NotInitializedException("The API has not yet been initialized.");
}
},
/**
* Sets an event listener for when the server sends a message and
* the message type is one of the types in types
*
* @param listener - The function to fire when a message of types is received
* @param {string[]} types - (OPTIONAL) Only fire the listener when the message type is in this array
* @param {Object} timeout - (OPTIONAL) The function(.ontimeout) to call after MS(.ms) of no reply
*
* TODO: Maybe a timeout will be needed? Pass in a function and a MS count.
*/
setMessageListener : function(listener, types, timeout) {
eventTypes = types;
this.socket.onmessage = function(message) {
var data = JSON.parse(message.data);
if (eventTypes) {
if(eventTypes.indexOf(data.command) !== -1) { // if contains
listener(data);
}
}
else {
listener(data);
}
};
},
/**
* Adds types to the types to listen for in the message event listener
*
* @param {string[]} types - The types to add
*/
addEventTypes : function(types) {
eventTypes = eventTypes.concat(types);
},
/**
* Removes the message event listener
*/
removeMessageListener : function() {
this.socket.onmessage = null;
}
};
utils
utils/formatDate.js
/* global DEFAULT_DATE_FORMAT */
/**
* Formats a Date object based on a format string, e.g., "yyyy/MM/dd hh:mm:ss"
* Original source:
* https://dzone.com/articles/javascript-formatdate-function
* Original source modified to fix a few bugs and modernize.
*
* @param {Date} date - the Date to format
* @param {String} formatString - the format string to use
* @returns {String} - the formatted date
*/
const formatDate = function (date, formatString=DEFAULT_DATE_FORMAT) {
if(date instanceof Date) {
const months = new Array("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec");
const yyyy = date.getFullYear();
const yy = yyyy.toString().slice(-2);
const M = date.getMonth() + 1;
const MM = M < 10 ? `0${M}` : M;
const MMM = months[M - 1];
const d = date.getDate();
const dd = d < 10 ? `0${d}` : d;
const h = date.getHours();
const hh = h < 10 ? `0${h}` : h;
const m = date.getMinutes();
const mm = m < 10 ? `0${m}` : m;
const s = date.getSeconds();
const ss = s < 10 ? `0${s}` : s;
formatString = formatString.replace(/yyyy/, yyyy);
formatString = formatString.replace(/yy/, yy);
formatString = formatString.replace(/MMM/, MMM);
formatString = formatString.replace(/MM/, MM);
formatString = formatString.replace(/M/, M);
formatString = formatString.replace(/dd/, dd);
formatString = formatString.replace(/d/, d);
formatString = formatString.replace(/hh/, hh);
formatString = formatString.replace(/h/, h);
formatString = formatString.replace(/mm/, mm);
formatString = formatString.replace(/m/, m);
formatString = formatString.replace(/ss/, ss);
formatString = formatString.replace(/s/, s);
return formatString;
} else {
return "";
}
};
utils/loadHtml.js
/* global fetch, DEBUG */
"use strict";
/*
* Replicates the functionality of jQuery's `load` function,
* used to load some HTML from another file into the current one.
*
* Based on this Stack Overflow answer:
* https://stackoverflow.com/a/38132775/3626537
* And `fetch` documentation:
* https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
*
* @param {string} parentElementId - The ID of the DOM element to load into
* @param {string} htmlFilePath - The path of the HTML file to load
*/
const loadHtml = function (parentElementId, filePath) {
const init = {
method: "GET",
headers: { "Content-Type": "text/html" },
mode: "cors",
cache: "default"
};
// Return Promise from `fetch` allows to use `.then` after call.
return fetch(filePath, init)
.then(function (response) {
return response.text();
})
.then(function (body) {
// Replace `#` char in case the function gets called `querySelector` or jQuery style
if (parentElementId.startsWith("#")) {
parentElementId.replace("#", "");
}
document.getElementById(parentElementId).innerHTML = body;
if (DEBUG) {
console.log(`File "${filePath}" loaded into element ID "${parentElementId}"`);
}
})
.catch(function(err) {
throw new FailureToLoadHTMLException(
`Could not load "${filePath} ` +
`into element ID "${parentElementId}"` +
`\n${err}`
);
});
};
const FailureToLoadHTMLException = function(message) {
this.name = "FailureToLoadHTMLException";
this.message = message;
this.stack = (new Error()).stack;
};
FailureToLoadHTMLException.prototype = new Error;
utils/logDebugMessage.js
/* global DEFAULT_DATE_FORMAT */
/**
* Log a debug message to the browser's JavaScript console.
* @param {String} msg
* @param {String} dateFormat
* @returns {undefined}
*/
const logDebugMessage = function(msg, dateFormat=DEFAULT_DATE_FORMAT) {
const timestamp = new Date();
console.log(`DEBUG | ${formatDate(timestamp, dateFormat)} | ${msg}`);
};