2023-10-18 16:51:32 -04:00
|
|
|
class WSElement extends HTMLElement {
|
|
|
|
static #sockets = {};
|
2023-10-19 14:51:58 -04:00
|
|
|
#socket;
|
2023-10-18 16:51:32 -04:00
|
|
|
#delay = 1000;
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
|
|
|
connectedCallback() {
|
|
|
|
// track `src` and `autoreconnect` attributes for later use
|
|
|
|
// `autoreconnect` defaults to `true`, unless explicitly set to `false`
|
|
|
|
this.src = this.getAttribute('src');
|
|
|
|
this.autoreconnect = !this.hasAttribute('autoreconnect') || this.getAttribute('autoreconnect').toLowerCase() !== 'false';
|
|
|
|
|
|
|
|
this.#connect_socket(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
#connect_socket(recreate) {
|
|
|
|
// `recreate` is used to handle reconnects, removeing the old websocket object
|
|
|
|
// and recreating a new one
|
|
|
|
if(recreate && WSElement.#sockets[this.src]) {
|
|
|
|
delete WSElement.#sockets[this.src];
|
|
|
|
}
|
|
|
|
|
|
|
|
// a single WebSocket can be reused for multiple elements
|
|
|
|
// check to see if one exists for this `src` else create
|
|
|
|
// a new one.
|
|
|
|
if(!(this.src in WSElement.#sockets)) {
|
2023-10-19 14:51:58 -04:00
|
|
|
this.#socket = WSElement.#newSocket(this.src);
|
|
|
|
WSElement.#sockets[this.src] = this.#socket;
|
2023-10-18 16:51:32 -04:00
|
|
|
} else {
|
2023-10-19 14:51:58 -04:00
|
|
|
this.#socket = WSElement.#sockets[this.src];
|
2023-10-18 16:51:32 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// setup event handlers for this WebSocket/Elemnent pair
|
2023-10-19 14:51:58 -04:00
|
|
|
this.#socket.addEventListener('error', this.#on_ws_error.bind(this));
|
|
|
|
this.#socket.addEventListener('message', this.#on_ws_message.bind(this));
|
|
|
|
this.#socket.addEventListener('open', this.#on_ws_open.bind(this));
|
|
|
|
this.#socket.addEventListener('close', this.#on_ws_close.bind(this));
|
2023-10-18 16:51:32 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#on_ws_open(sock) {
|
|
|
|
// set connected attribute for use in CSS
|
|
|
|
// ```
|
|
|
|
// ws-element[connected] {
|
|
|
|
// color: green;
|
|
|
|
// }
|
|
|
|
// ws-element:not(connected) {
|
|
|
|
// color: red;
|
|
|
|
// }
|
|
|
|
// ```
|
|
|
|
this.setAttribute('connected', '');
|
|
|
|
|
|
|
|
// call this.open if defined by subclass
|
|
|
|
if('open' in this) {
|
|
|
|
this.open(sock);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#on_ws_message(sock) {
|
|
|
|
// call this.message, which is always defined
|
|
|
|
this.message(sock.data);
|
|
|
|
}
|
|
|
|
|
|
|
|
#on_ws_close(sock) {
|
|
|
|
this.removeAttribute('connected');
|
|
|
|
|
|
|
|
// clean up event handlers for this element
|
|
|
|
sock.target.removeEventListener('error', this.#on_ws_error.bind(this));
|
|
|
|
sock.target.removeEventListener('message', this.#on_ws_message.bind(this));
|
|
|
|
sock.target.removeEventListener('open', this.#on_ws_open.bind(this));
|
|
|
|
sock.target.removeEventListener('close', this.#on_ws_close.bind(this));
|
|
|
|
|
|
|
|
// call this.close if subclass has defined it
|
|
|
|
if('close' in this) {
|
|
|
|
this.close(sock);
|
|
|
|
}
|
|
|
|
|
|
|
|
// handle autoreconnect and delay throttling
|
|
|
|
if(this.autoreconnect) {
|
|
|
|
setTimeout(this.#connect_socket.call(this, true), this.#delay);
|
|
|
|
if(this.#delay < 10000) {
|
|
|
|
this.#delay += 1000;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#on_ws_error(sock) {
|
|
|
|
// call this.error if subclass has defined it
|
|
|
|
if('error' in this) {
|
|
|
|
this.error(sock);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// default behavior on this.message is just update innerText
|
|
|
|
message(data) {
|
|
|
|
this.innerText = data;
|
|
|
|
}
|
|
|
|
|
|
|
|
// static method for creating new sockets, which are tracked
|
|
|
|
// on the static propert WSElement.#sockets
|
|
|
|
static #newSocket(src) {
|
|
|
|
var s = window.location.protocol.slice(-2) === 'p:' ? '' : 's';
|
|
|
|
var host = window.location.host;
|
|
|
|
var path = src[0] === '/' ? src.slice(1) : src;
|
|
|
|
var wsurl = `ws${s}://${host}/${path}`;
|
|
|
|
|
|
|
|
return new WebSocket(wsurl);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Example subclass to display a basic 'Time: 00:00:00'
|
|
|
|
class WSTime extends WSElement {
|
|
|
|
connectedCallback() {
|
|
|
|
super.connectedCallback();
|
|
|
|
|
|
|
|
// insert subelements for the label (Time:) and the value;
|
|
|
|
this.insertAdjacentHTML('beforeend', '<label class="time-label">Time: </label><span class="time-value"></span>');
|
|
|
|
// track the value element for updating later
|
|
|
|
this.value = this.querySelector('span.time-value');
|
|
|
|
}
|
|
|
|
|
|
|
|
// override this.message to update the innerText of the value span element
|
|
|
|
message(data) {
|
|
|
|
this.value.innerText = data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Example subclass to parse arbitrary json and build a table
|
|
|
|
// with dynamically controlled limit
|
|
|
|
class WSTable extends WSElement {
|
|
|
|
// observe limit to dynamically update
|
|
|
|
static observedAttributes = [ 'limit' ];
|
|
|
|
|
|
|
|
connectedCallback() {
|
|
|
|
super.connectedCallback();
|
|
|
|
|
|
|
|
// ignoreNext is to handle duplicate headers being sent on reconnect
|
|
|
|
this.ignoreNext = false;
|
|
|
|
|
|
|
|
// insert and track table element
|
|
|
|
this.insertAdjacentHTML('beforeend', '<table class="table"></table>');
|
|
|
|
this.table = this.querySelector('table');
|
|
|
|
|
|
|
|
// track limit attribute, defaulting to no limit
|
|
|
|
if(this.hasAttribute('limit')) {
|
|
|
|
this.limit = parseInt(this.getAttribute('limit')) || -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
|
|
// update the tracked limit, defaulting to none if no value provided
|
|
|
|
if(oldValue === null) {
|
|
|
|
this.limit = parseInt(newValue) || -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
message(data) {
|
|
|
|
// ignore one message, because the demo server is a bit dumb
|
|
|
|
if(this.ignoreNext) {
|
|
|
|
this.ignoreNext = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
data = JSON.parse(data);
|
|
|
|
|
|
|
|
// with autoheader set the first message will create a header row instead of body
|
|
|
|
if(this.hasAttribute('autoheader') && !('header' in this)) {
|
|
|
|
// createElement works just as well as insertAdjacentHTML for single, to be tracked elements
|
|
|
|
this.thead = document.createElement('thead');
|
|
|
|
this.header = document.createElement('tr');
|
|
|
|
|
|
|
|
this.thead.appendChild(this.header);
|
|
|
|
this.table.appendChild(this.thead);
|
|
|
|
|
|
|
|
// add a `th` for each key/value pair in the json data
|
|
|
|
for(let k in data) {
|
|
|
|
this.header.insertAdjacentHTML('beforeend', `<th scope="col" class="${k}">${data[k]}</th>`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// work is done, this block will be ignored in future messages
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// create the body if it doesn't yet exists
|
|
|
|
if(!('tbody' in this)) {
|
|
|
|
this.tbody = document.createElement('tbody');
|
|
|
|
this.table.appendChild(this.tbody);
|
|
|
|
}
|
|
|
|
|
|
|
|
// create TD for each key/value pair in message data
|
|
|
|
var inner = '';
|
|
|
|
for(let k in data) {
|
|
|
|
inner += `<td class="${k}">${data[k]}</td>`;
|
|
|
|
}
|
|
|
|
// instead the above TDs into a new TR
|
|
|
|
this.tbody.insertAdjacentHTML('beforeend', `<tr>${inner}</tr>`);
|
|
|
|
|
|
|
|
// check limits are enforced
|
|
|
|
if(this.limit > -1 && this.tbody.childElementCount > this.limit) {
|
|
|
|
while(this.tbody.childElementCount > this.limit) {
|
|
|
|
this.tbody.removeChild(this.tbody.childNodes[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ignore the next message if we're disconnected,
|
|
|
|
// demo server always sends headers on connect
|
|
|
|
close(sock) {
|
|
|
|
this.ignoreNext = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// define custom elements
|
|
|
|
customElements.define('ws-element', WSElement);
|
|
|
|
customElements.define('ws-time', WSTime);
|
|
|
|
customElements.define('ws-table', WSTable);
|