361 lines
13 KiB
HTML
361 lines
13 KiB
HTML
|
<!DOCTYPE html>
|
||
|
<html lang="en">
|
||
|
<head>
|
||
|
<meta charset="UTF-8">
|
||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||
|
<title>WebSocket</title>
|
||
|
<link rel="stylesheet" href="css/milligram.min.css">
|
||
|
<style>
|
||
|
.page {margin: 0 auto; width: 800px}
|
||
|
.mark {color: mediumseagreen; padding: .5rem}
|
||
|
.mark a {color: mediumseagreen}
|
||
|
|
||
|
.alert-ok {background-color: mediumseagreen; color: #222; padding: .5rem;text-align: center;}
|
||
|
.alert-error {background-color: indianred; color: #EEE; padding: .5rem;text-align: center;}
|
||
|
.alert-warn {background-color: darkkhaki; color: #222; padding: .5rem;text-align: center;}
|
||
|
|
||
|
.card {
|
||
|
display: flex;
|
||
|
min-width: 276px;
|
||
|
width: 100%;
|
||
|
flex-direction: column;
|
||
|
}
|
||
|
|
||
|
.card-header {
|
||
|
font-weight: bold;
|
||
|
margin: 0;
|
||
|
padding: 1em;
|
||
|
}
|
||
|
|
||
|
.card-content {
|
||
|
flex: 1 1 auto;
|
||
|
padding: 1em;
|
||
|
}
|
||
|
|
||
|
.card-footer {
|
||
|
display: flex;
|
||
|
align-items: stretch;
|
||
|
}
|
||
|
|
||
|
.modal {
|
||
|
position: fixed;
|
||
|
background: rgba(0, 0, 0, .6);
|
||
|
top: 0;
|
||
|
left: 0;
|
||
|
right: 0;
|
||
|
bottom: 0;
|
||
|
padding: 1rem;
|
||
|
display: none;
|
||
|
align-items: center;
|
||
|
justify-content: center;
|
||
|
flex-direction: column;
|
||
|
}
|
||
|
|
||
|
.modal.active {
|
||
|
display: flex;
|
||
|
}
|
||
|
|
||
|
.modal-content {
|
||
|
display: flex;
|
||
|
width: 28rem;
|
||
|
max-width: 80vw;
|
||
|
height: 20rem;
|
||
|
background: white;
|
||
|
}
|
||
|
|
||
|
/* Theme */
|
||
|
button, input[type=submit] {background-color: steelblue; border: 0.1rem solid steelblue;}
|
||
|
input[type=text]:focus,select:focus {border: 1px solid steelblue}
|
||
|
table tbody tr:nth-child(odd) {
|
||
|
background-color: #EEE;
|
||
|
}
|
||
|
table tbody td:first-child {padding-left: .75rem}
|
||
|
|
||
|
.weak {color: #767676; font-size: .75em}
|
||
|
.uppercase {text-transform: uppercase;}
|
||
|
</style>
|
||
|
</head>
|
||
|
<body>
|
||
|
|
||
|
<div class="page">
|
||
|
<div id="wc">
|
||
|
<div :if="this.error" class="alert-error">{this.error}</div>
|
||
|
|
||
|
<!-- Connected-->
|
||
|
<div :if="this.connect.socket">
|
||
|
|
||
|
<div :if="this.connect.status">
|
||
|
<small class="mark">
|
||
|
<strong>CONNECTED:</strong> {this.connected} <strong>AS:</strong> {this.connect.as}
|
||
|
<span style="float:right;padding-right: 1rem">
|
||
|
<a @click="showClients">CLIENTS ({this.clients.length})</a>
|
||
|
| <a @click="disconnect">DISCONNECT</a>
|
||
|
</span>
|
||
|
</small>
|
||
|
|
||
|
<form action="">
|
||
|
<h3>Send Message</h3>
|
||
|
|
||
|
<label for="">Type</label>
|
||
|
<select @bind="message.type">
|
||
|
<option value="message">Message</option>
|
||
|
<option value="broadcast">Broadcast</option>
|
||
|
<option value="notification">Notification</option>
|
||
|
</select>
|
||
|
|
||
|
<div :if="this.message.type === 'message'">
|
||
|
<label for="">User</label>
|
||
|
<input type="text" @bind="message.to" value="{this.message.to}">
|
||
|
</div>
|
||
|
|
||
|
<div :if="this.message.type === 'notification'">
|
||
|
<label for="">User</label>
|
||
|
<input type="text" @bind="message.to" value="{this.message.to}">
|
||
|
|
||
|
<label for="">Title</label>
|
||
|
<input type="text" @bind="message.subject" value="{this.message.subject}">
|
||
|
</div>
|
||
|
|
||
|
<label for="">Message</label>
|
||
|
<input type="text" @bind="message.body">
|
||
|
<input type="submit" @click="send" value="Send">
|
||
|
</form>
|
||
|
</div>
|
||
|
<div :else class="alert-ok">
|
||
|
<strong>CONNECTING:</strong> {this.connected} <strong>AS:</strong> {this.connect.as}
|
||
|
</div>
|
||
|
</div>
|
||
|
<!-- No Connection -->
|
||
|
<div :else>
|
||
|
<div :if="this.connect.retry > 0 && this.connect.retry < 6" class="alert-warn">
|
||
|
<strong>RECONNECTING IN {this.connect.timeout / 1000 + 's'} ATTEMPT</strong> {this.connect.retry}
|
||
|
</div>
|
||
|
<div :else>
|
||
|
<p>
|
||
|
<h3>No Connection</h3>
|
||
|
<label for="">Type</label>
|
||
|
<select @bind="connect.scheme">
|
||
|
<option value="ws://">ws://</option>
|
||
|
<option value="wss://">wss://</option>
|
||
|
</select>
|
||
|
<label for="">URL</label>
|
||
|
<input type="text" @bind="connect.url" value="{this.connect.url}">
|
||
|
<label for="">Connect Alias</label>
|
||
|
<input type="text" @bind="connect.as" value="{this.connect.as}">
|
||
|
<input @click="connect" type="submit" value="Connect">
|
||
|
</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<!-- Messages -->
|
||
|
<div :if="this.messages.length">
|
||
|
<h3>Messages</h3>
|
||
|
<table>
|
||
|
<thead>
|
||
|
<tr>
|
||
|
<th>Type</th>
|
||
|
<th>From</th>
|
||
|
<th style="width: 50%">Message</th>
|
||
|
<th>Timestamp</th>
|
||
|
</tr>
|
||
|
</thead>
|
||
|
<tbody>
|
||
|
<tr :for="message in this.messages.reverse()" class="highlight: message.from === ">
|
||
|
<td class="weak uppercase">{message.type}</td>
|
||
|
<td class="weak">{message.from}</td>
|
||
|
<td>
|
||
|
<strong>{message.body}</strong>
|
||
|
</td>
|
||
|
<td class="weak">{message.date}</td>
|
||
|
</tr>
|
||
|
</tbody>
|
||
|
</table>
|
||
|
</div>
|
||
|
|
||
|
<div class="modal">
|
||
|
<div class="modal-content" style="background: #FFF">
|
||
|
<div class="card">
|
||
|
<div class="card-header">
|
||
|
Clients ({this.clients.length})
|
||
|
<span style="float: right; cursor: pointer;" @click="showClients">X</span>
|
||
|
</div>
|
||
|
<div class="card-content">
|
||
|
<table>
|
||
|
<tr :for="client in this.clients">
|
||
|
<th>{client.user}</th>
|
||
|
<th>{client.status}</th>
|
||
|
</tr>
|
||
|
</table>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
</div>
|
||
|
|
||
|
<script type="module">
|
||
|
|
||
|
import Litedom from './js/litedom.es.js';
|
||
|
|
||
|
Litedom({
|
||
|
el: '#wc',
|
||
|
data: {
|
||
|
messages: [],
|
||
|
clients: [],
|
||
|
message: {
|
||
|
subject: 'Return to Yard Request',
|
||
|
body: '',
|
||
|
to: '0000000',
|
||
|
type: 'message',
|
||
|
data: {}
|
||
|
},
|
||
|
error: null,
|
||
|
connect: {
|
||
|
as: '1111111', // TODO: set or randomize
|
||
|
scheme: 'ws://',
|
||
|
url: 'localhost:3001',
|
||
|
socket: null,
|
||
|
status: null,
|
||
|
retry: 0,
|
||
|
timeout: 5000
|
||
|
},
|
||
|
connected: (state) => state.connect.scheme + state.connect.url,
|
||
|
},
|
||
|
connect(e) {
|
||
|
let scheme = this.data.connect.scheme;
|
||
|
let url = this.data.connect.url;
|
||
|
let socket = new WebSocket(scheme+url);
|
||
|
|
||
|
// HACK: cannot use litedom data with websockets
|
||
|
window.ws = socket;
|
||
|
|
||
|
// update binds
|
||
|
this.data.connect.socket = socket;
|
||
|
this.data.error = null;
|
||
|
|
||
|
if(socket) {
|
||
|
|
||
|
socket.onopen = (e) => {
|
||
|
console.log('CONNECTED');
|
||
|
this.data.connect.status = true;
|
||
|
|
||
|
// reset retrys
|
||
|
this.data.connect.retry = 0;
|
||
|
|
||
|
// register client after connection
|
||
|
socket.send(JSON.stringify({
|
||
|
user: this.data.connect.as,
|
||
|
type: 'register'
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
socket.onmessage = (e) => {
|
||
|
console.log('MESSAGE:', e.data);
|
||
|
|
||
|
try {
|
||
|
let payload = JSON.parse(e.data);
|
||
|
|
||
|
switch(payload.type) {
|
||
|
case "broadcast":
|
||
|
case "message":
|
||
|
this.data.messages.push(payload);
|
||
|
break;
|
||
|
case "offline":
|
||
|
this.data.error = "Server Went Offline";
|
||
|
break;
|
||
|
case "clients":
|
||
|
this.data.clients = payload.data;
|
||
|
break;
|
||
|
default:
|
||
|
console.log("UNDEFINED", payload);
|
||
|
}
|
||
|
|
||
|
} catch(err) {
|
||
|
console.warn(err);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
socket.onclose = (e) => {
|
||
|
this.data.error = `Connection Closed (${e.code} ${e.reason ? '- ' + e.reason : ''})`;
|
||
|
|
||
|
this.data.connect.socket = null;
|
||
|
ws = null;
|
||
|
|
||
|
// try to reconnect
|
||
|
this.reconnect();
|
||
|
}
|
||
|
|
||
|
socket.onerror = (e) => {
|
||
|
this.data.error = "Socket Error";
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
disconnect() {
|
||
|
this.data.connect.retry = false;
|
||
|
ws.close(1000, 'Going Bye-Bye!');
|
||
|
},
|
||
|
reconnect() {
|
||
|
// only reconnect if enabled
|
||
|
if(this.data.connect.retry === false) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
this.data.connect.retry += 1;
|
||
|
|
||
|
if(this.data.connect.retry < 6) {
|
||
|
// wait before retrying
|
||
|
setTimeout(() => {
|
||
|
console.log("RECONNECTION:ATTEMPT", this.data.connect.retry);
|
||
|
this.connect();
|
||
|
}, this.data.connect.timeout);
|
||
|
|
||
|
} else {
|
||
|
if(this.data.connect.retry >= 5) {
|
||
|
console.log("RECONNECTION:EXHAUSTED");
|
||
|
|
||
|
this.data.error = "RECONNECTION ATTEMPTS EXHAUSTED";
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
send(e) {
|
||
|
if(ws.readyState === WebSocket.OPEN) {
|
||
|
|
||
|
switch(this.data.message.type) {
|
||
|
case "message":
|
||
|
case "broadcast":
|
||
|
let payload = {
|
||
|
"from": this.data.connect.as,
|
||
|
"to": this.data.message.to,
|
||
|
"type": this.data.message.type,
|
||
|
"body": this.data.message.body,
|
||
|
"data": this.data.message.data
|
||
|
};
|
||
|
return ws.send(JSON.stringify(payload));
|
||
|
case "notification":
|
||
|
return ws.send(JSON.stringify({
|
||
|
"to": this.data.message.to,
|
||
|
"from": this.data.connect.as,
|
||
|
"type": this.data.message.type,
|
||
|
"subject": this.data.message.subject,
|
||
|
"body": this.data.message.body,
|
||
|
"data": this.data.message.data || {}
|
||
|
}));
|
||
|
default:
|
||
|
console.log("UNDEFINED:TYPE", this.data.message);
|
||
|
}
|
||
|
|
||
|
} else {
|
||
|
this.data.connect.socket = null;
|
||
|
this.data.error = "Connection Lost";
|
||
|
}
|
||
|
},
|
||
|
showClients() {
|
||
|
var modal = document.querySelector('.modal');
|
||
|
modal.classList.toggle('active');
|
||
|
}
|
||
|
});
|
||
|
</script>
|
||
|
</body>
|
||
|
</html>
|