Cardshifter login page using vanilla JavaScript

Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
8
down vote
favorite
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.jsserver_interface/server_interface.jsutils/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) ;
/*
* 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 ;
javascript html websocket
add a comment |Â
up vote
8
down vote
favorite
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.jsserver_interface/server_interface.jsutils/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) ;
/*
* 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 ;
javascript html websocket
2
I have no desire to use libraries like jQuery, Underscore, etc. you must be having a lot of free time ;-]
â t3chb0t
Mar 24 at 21:28
3
I'm not in a hurry or anything, I do want to prove as a personal challenge that you can, in fact, write a good and dynamic web app with just regular old JavaScript :)
â Phrancis
Mar 24 at 21:34
3
Of course you can. Those libraries are just components built out of regular old JS. It's just how much of the wheel you want to reinvent.
â Hosch250
Mar 24 at 22:37
add a comment |Â
up vote
8
down vote
favorite
up vote
8
down vote
favorite
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.jsserver_interface/server_interface.jsutils/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) ;
/*
* 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 ;
javascript html websocket
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.jsserver_interface/server_interface.jsutils/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) ;
/*
* 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 ;
javascript html websocket
edited Mar 24 at 21:41
asked Mar 24 at 21:19
Phrancis
14.6k645137
14.6k645137
2
I have no desire to use libraries like jQuery, Underscore, etc. you must be having a lot of free time ;-]
â t3chb0t
Mar 24 at 21:28
3
I'm not in a hurry or anything, I do want to prove as a personal challenge that you can, in fact, write a good and dynamic web app with just regular old JavaScript :)
â Phrancis
Mar 24 at 21:34
3
Of course you can. Those libraries are just components built out of regular old JS. It's just how much of the wheel you want to reinvent.
â Hosch250
Mar 24 at 22:37
add a comment |Â
2
I have no desire to use libraries like jQuery, Underscore, etc. you must be having a lot of free time ;-]
â t3chb0t
Mar 24 at 21:28
3
I'm not in a hurry or anything, I do want to prove as a personal challenge that you can, in fact, write a good and dynamic web app with just regular old JavaScript :)
â Phrancis
Mar 24 at 21:34
3
Of course you can. Those libraries are just components built out of regular old JS. It's just how much of the wheel you want to reinvent.
â Hosch250
Mar 24 at 22:37
2
2
I have no desire to use libraries like jQuery, Underscore, etc. you must be having a lot of free time ;-]
â t3chb0t
Mar 24 at 21:28
I have no desire to use libraries like jQuery, Underscore, etc. you must be having a lot of free time ;-]
â t3chb0t
Mar 24 at 21:28
3
3
I'm not in a hurry or anything, I do want to prove as a personal challenge that you can, in fact, write a good and dynamic web app with just regular old JavaScript :)
â Phrancis
Mar 24 at 21:34
I'm not in a hurry or anything, I do want to prove as a personal challenge that you can, in fact, write a good and dynamic web app with just regular old JavaScript :)
â Phrancis
Mar 24 at 21:34
3
3
Of course you can. Those libraries are just components built out of regular old JS. It's just how much of the wheel you want to reinvent.
â Hosch250
Mar 24 at 22:37
Of course you can. Those libraries are just components built out of regular old JS. It's just how much of the wheel you want to reinvent.
â Hosch250
Mar 24 at 22:37
add a comment |Â
1 Answer
1
active
oldest
votes
up vote
4
down vote
accepted
Very neat project! I really like that you are avoiding using a library, it's a great way to get better at JS.
I see the
if (DEBUG) logDebugMessage()pattern several times, it might be worth bringing the conditional inside thelogDebugMessagefunction.formatDatehas a ton of repetitive code, I'd recommend taking advantage of.replace's second parameter. There's probably some way to reduce this further...const formatDate = function (date, formatString = DEFAULT_DATE_FORMAT) ss;The original
formatDatefunction will have a problem in May ifMMMis included in the format string.It might be worth allowing
formatDateto accept no arguments, in which case it uses the current date.loadHtmllooks good to me with two exceptions. First, if the passed id starts with#, the#will not be removed as the comment implies it should. Second, if you define alogDebugMessagefunction, use it! Drop theconsole.log.In
server_interface.js, I'd recommend the source of theflattenmethod, it explains how it works.Get rid of unused variables. There are plenty of tools which can check if a variable is used, you should get rid of them instead of just commenting about them.
Since you are using
letandconst, you can useArray.prototype.includesinstead ofArray.prototype.indexOfto check for an element in an array.The
ChatMessagetoStringmethod won't work. JS isn't C# - it's not possible to dropthisand still access the instance variables. Same goes forPlayerConfigMessage.Instead of using the
LoginMessage : function(username)pattern, you can useLoginMessage(username).wsProtocolFinderdoes not check forwssat the start of the string. It will also matchwssssss://. The regex should be/^wss?:///.I know the goal of this project is to avoid any npm packages, however you should really consider at least using a linting program (if only installed locally). Pretty much all linting programs could have caught the problems with
thismentioned above and can also alert you to the unused variables.
Nice answer, thank you so much! I use Netbeans as IDE which includes JsHint, but apparently it didn't catch all that. Any suggestions on a linter/plugin I can use?
â Phrancis
Mar 25 at 19:15
@Phrancis good question! JSHint has an undef option which should warn you about this, though you may need to enable it. I'm not familiar with Netbeans (VSCode here) so can't really recommend how to set that up easily.
â Gerrit0
Mar 25 at 21:57
Actually, JsHint did warn me, but I realize I ignored it lol.
â Phrancis
Mar 25 at 22:28
add a comment |Â
1 Answer
1
active
oldest
votes
1 Answer
1
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
4
down vote
accepted
Very neat project! I really like that you are avoiding using a library, it's a great way to get better at JS.
I see the
if (DEBUG) logDebugMessage()pattern several times, it might be worth bringing the conditional inside thelogDebugMessagefunction.formatDatehas a ton of repetitive code, I'd recommend taking advantage of.replace's second parameter. There's probably some way to reduce this further...const formatDate = function (date, formatString = DEFAULT_DATE_FORMAT) ss;The original
formatDatefunction will have a problem in May ifMMMis included in the format string.It might be worth allowing
formatDateto accept no arguments, in which case it uses the current date.loadHtmllooks good to me with two exceptions. First, if the passed id starts with#, the#will not be removed as the comment implies it should. Second, if you define alogDebugMessagefunction, use it! Drop theconsole.log.In
server_interface.js, I'd recommend the source of theflattenmethod, it explains how it works.Get rid of unused variables. There are plenty of tools which can check if a variable is used, you should get rid of them instead of just commenting about them.
Since you are using
letandconst, you can useArray.prototype.includesinstead ofArray.prototype.indexOfto check for an element in an array.The
ChatMessagetoStringmethod won't work. JS isn't C# - it's not possible to dropthisand still access the instance variables. Same goes forPlayerConfigMessage.Instead of using the
LoginMessage : function(username)pattern, you can useLoginMessage(username).wsProtocolFinderdoes not check forwssat the start of the string. It will also matchwssssss://. The regex should be/^wss?:///.I know the goal of this project is to avoid any npm packages, however you should really consider at least using a linting program (if only installed locally). Pretty much all linting programs could have caught the problems with
thismentioned above and can also alert you to the unused variables.
Nice answer, thank you so much! I use Netbeans as IDE which includes JsHint, but apparently it didn't catch all that. Any suggestions on a linter/plugin I can use?
â Phrancis
Mar 25 at 19:15
@Phrancis good question! JSHint has an undef option which should warn you about this, though you may need to enable it. I'm not familiar with Netbeans (VSCode here) so can't really recommend how to set that up easily.
â Gerrit0
Mar 25 at 21:57
Actually, JsHint did warn me, but I realize I ignored it lol.
â Phrancis
Mar 25 at 22:28
add a comment |Â
up vote
4
down vote
accepted
Very neat project! I really like that you are avoiding using a library, it's a great way to get better at JS.
I see the
if (DEBUG) logDebugMessage()pattern several times, it might be worth bringing the conditional inside thelogDebugMessagefunction.formatDatehas a ton of repetitive code, I'd recommend taking advantage of.replace's second parameter. There's probably some way to reduce this further...const formatDate = function (date, formatString = DEFAULT_DATE_FORMAT) ss;The original
formatDatefunction will have a problem in May ifMMMis included in the format string.It might be worth allowing
formatDateto accept no arguments, in which case it uses the current date.loadHtmllooks good to me with two exceptions. First, if the passed id starts with#, the#will not be removed as the comment implies it should. Second, if you define alogDebugMessagefunction, use it! Drop theconsole.log.In
server_interface.js, I'd recommend the source of theflattenmethod, it explains how it works.Get rid of unused variables. There are plenty of tools which can check if a variable is used, you should get rid of them instead of just commenting about them.
Since you are using
letandconst, you can useArray.prototype.includesinstead ofArray.prototype.indexOfto check for an element in an array.The
ChatMessagetoStringmethod won't work. JS isn't C# - it's not possible to dropthisand still access the instance variables. Same goes forPlayerConfigMessage.Instead of using the
LoginMessage : function(username)pattern, you can useLoginMessage(username).wsProtocolFinderdoes not check forwssat the start of the string. It will also matchwssssss://. The regex should be/^wss?:///.I know the goal of this project is to avoid any npm packages, however you should really consider at least using a linting program (if only installed locally). Pretty much all linting programs could have caught the problems with
thismentioned above and can also alert you to the unused variables.
Nice answer, thank you so much! I use Netbeans as IDE which includes JsHint, but apparently it didn't catch all that. Any suggestions on a linter/plugin I can use?
â Phrancis
Mar 25 at 19:15
@Phrancis good question! JSHint has an undef option which should warn you about this, though you may need to enable it. I'm not familiar with Netbeans (VSCode here) so can't really recommend how to set that up easily.
â Gerrit0
Mar 25 at 21:57
Actually, JsHint did warn me, but I realize I ignored it lol.
â Phrancis
Mar 25 at 22:28
add a comment |Â
up vote
4
down vote
accepted
up vote
4
down vote
accepted
Very neat project! I really like that you are avoiding using a library, it's a great way to get better at JS.
I see the
if (DEBUG) logDebugMessage()pattern several times, it might be worth bringing the conditional inside thelogDebugMessagefunction.formatDatehas a ton of repetitive code, I'd recommend taking advantage of.replace's second parameter. There's probably some way to reduce this further...const formatDate = function (date, formatString = DEFAULT_DATE_FORMAT) ss;The original
formatDatefunction will have a problem in May ifMMMis included in the format string.It might be worth allowing
formatDateto accept no arguments, in which case it uses the current date.loadHtmllooks good to me with two exceptions. First, if the passed id starts with#, the#will not be removed as the comment implies it should. Second, if you define alogDebugMessagefunction, use it! Drop theconsole.log.In
server_interface.js, I'd recommend the source of theflattenmethod, it explains how it works.Get rid of unused variables. There are plenty of tools which can check if a variable is used, you should get rid of them instead of just commenting about them.
Since you are using
letandconst, you can useArray.prototype.includesinstead ofArray.prototype.indexOfto check for an element in an array.The
ChatMessagetoStringmethod won't work. JS isn't C# - it's not possible to dropthisand still access the instance variables. Same goes forPlayerConfigMessage.Instead of using the
LoginMessage : function(username)pattern, you can useLoginMessage(username).wsProtocolFinderdoes not check forwssat the start of the string. It will also matchwssssss://. The regex should be/^wss?:///.I know the goal of this project is to avoid any npm packages, however you should really consider at least using a linting program (if only installed locally). Pretty much all linting programs could have caught the problems with
thismentioned above and can also alert you to the unused variables.
Very neat project! I really like that you are avoiding using a library, it's a great way to get better at JS.
I see the
if (DEBUG) logDebugMessage()pattern several times, it might be worth bringing the conditional inside thelogDebugMessagefunction.formatDatehas a ton of repetitive code, I'd recommend taking advantage of.replace's second parameter. There's probably some way to reduce this further...const formatDate = function (date, formatString = DEFAULT_DATE_FORMAT) ss;The original
formatDatefunction will have a problem in May ifMMMis included in the format string.It might be worth allowing
formatDateto accept no arguments, in which case it uses the current date.loadHtmllooks good to me with two exceptions. First, if the passed id starts with#, the#will not be removed as the comment implies it should. Second, if you define alogDebugMessagefunction, use it! Drop theconsole.log.In
server_interface.js, I'd recommend the source of theflattenmethod, it explains how it works.Get rid of unused variables. There are plenty of tools which can check if a variable is used, you should get rid of them instead of just commenting about them.
Since you are using
letandconst, you can useArray.prototype.includesinstead ofArray.prototype.indexOfto check for an element in an array.The
ChatMessagetoStringmethod won't work. JS isn't C# - it's not possible to dropthisand still access the instance variables. Same goes forPlayerConfigMessage.Instead of using the
LoginMessage : function(username)pattern, you can useLoginMessage(username).wsProtocolFinderdoes not check forwssat the start of the string. It will also matchwssssss://. The regex should be/^wss?:///.I know the goal of this project is to avoid any npm packages, however you should really consider at least using a linting program (if only installed locally). Pretty much all linting programs could have caught the problems with
thismentioned above and can also alert you to the unused variables.
answered Mar 25 at 3:02
Gerrit0
2,6601518
2,6601518
Nice answer, thank you so much! I use Netbeans as IDE which includes JsHint, but apparently it didn't catch all that. Any suggestions on a linter/plugin I can use?
â Phrancis
Mar 25 at 19:15
@Phrancis good question! JSHint has an undef option which should warn you about this, though you may need to enable it. I'm not familiar with Netbeans (VSCode here) so can't really recommend how to set that up easily.
â Gerrit0
Mar 25 at 21:57
Actually, JsHint did warn me, but I realize I ignored it lol.
â Phrancis
Mar 25 at 22:28
add a comment |Â
Nice answer, thank you so much! I use Netbeans as IDE which includes JsHint, but apparently it didn't catch all that. Any suggestions on a linter/plugin I can use?
â Phrancis
Mar 25 at 19:15
@Phrancis good question! JSHint has an undef option which should warn you about this, though you may need to enable it. I'm not familiar with Netbeans (VSCode here) so can't really recommend how to set that up easily.
â Gerrit0
Mar 25 at 21:57
Actually, JsHint did warn me, but I realize I ignored it lol.
â Phrancis
Mar 25 at 22:28
Nice answer, thank you so much! I use Netbeans as IDE which includes JsHint, but apparently it didn't catch all that. Any suggestions on a linter/plugin I can use?
â Phrancis
Mar 25 at 19:15
Nice answer, thank you so much! I use Netbeans as IDE which includes JsHint, but apparently it didn't catch all that. Any suggestions on a linter/plugin I can use?
â Phrancis
Mar 25 at 19:15
@Phrancis good question! JSHint has an undef option which should warn you about this, though you may need to enable it. I'm not familiar with Netbeans (VSCode here) so can't really recommend how to set that up easily.
â Gerrit0
Mar 25 at 21:57
@Phrancis good question! JSHint has an undef option which should warn you about this, though you may need to enable it. I'm not familiar with Netbeans (VSCode here) so can't really recommend how to set that up easily.
â Gerrit0
Mar 25 at 21:57
Actually, JsHint did warn me, but I realize I ignored it lol.
â Phrancis
Mar 25 at 22:28
Actually, JsHint did warn me, but I realize I ignored it lol.
â Phrancis
Mar 25 at 22:28
add a comment |Â
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f190405%2fcardshifter-login-page-using-vanilla-javascript%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
2
I have no desire to use libraries like jQuery, Underscore, etc. you must be having a lot of free time ;-]
â t3chb0t
Mar 24 at 21:28
3
I'm not in a hurry or anything, I do want to prove as a personal challenge that you can, in fact, write a good and dynamic web app with just regular old JavaScript :)
â Phrancis
Mar 24 at 21:34
3
Of course you can. Those libraries are just components built out of regular old JS. It's just how much of the wheel you want to reinvent.
â Hosch250
Mar 24 at 22:37