ctucx.git: mqtt-webui

webui for mqtt, can be used to control/display data in mqtt-topics

commit 527b3f597d915834933974424a0aeb6078adfc8a
parent a7b766c4280dd30e97a6d85bcf37be0048f21fcf
Author: Leah (ctucx) <git@ctu.cx>
Date: Mon, 12 Dec 2022 23:13:26 +0100

some improvements to js and css
2 files changed, 68 insertions(+), 50 deletions(-)
M
src/webui.css
|
72
+++++++++++++++++++++++++++++++++++++++++-------------------------------
M
src/webui.js
|
46
+++++++++++++++++++++++++++-------------------
diff --git a/src/webui.css b/src/webui.css
@@ -61,7 +61,7 @@ body {
 	color: var(--text-color);
 	text-align: left;
 	background-color: var(--body-background-color) !important;
-	padding-top: 70px;
+	padding-top: 5em;
 }
 
 a {

@@ -100,12 +100,12 @@ header {
 }
 
 .scroll {
-	border-bottom: 1px solid var(--nav-scroll-border-color);
+	border-bottom: .1em solid var(--nav-scroll-border-color);
 }
 
-nav a {
-	padding-top: 0.3125rem;
-	padding-bottom: 0.3125rem;
+nav span {
+	padding-top: .3125rem;
+	padding-bottom: .3125rem;
 	font-size: 1.25rem;
 	white-space: nowrap;
 }

@@ -129,7 +129,7 @@ button .icon {
 .icon {
 	height: 31px;
 	width: 31px;
-	margin: 4px;
+	margin: .25em;
 }
 
 .icon svg {

@@ -139,30 +139,31 @@ button .icon {
 }
 
 .page {
- 	margin-left: auto;
+	margin-left: auto;
 	margin-right: auto;
 	max-width: 50em;
 }
 
 section {
-	border-radius: 0.25rem;
-	border: 1px solid var(--section-border-color);
-	margin-bottom: 10px;
+	border-radius: .25em;
+	border: .1em solid var(--section-border-color);
+	margin-bottom: .6em;
 }
 
 section > div {
 	background-color: var(--section-background-color);
-	border-bottom: 1px solid var(--section-border-color);
-	padding: 0.25rem;
+	border-bottom: .1em solid var(--section-border-color);
+	padding: .25em;
 	color: var(--secondary-text-color);
 	display: flex;
-	align-self: center !important;
+	align-items: center;
+}
+
+section > div[data-type="html"] {
+	display: block;
 }
 
 section > div:first-child {
-	color: var(--text-color);
-	padding-left: 1em;
-	background-color: var(--section-header-background-color);
 	border-top-left-radius: inherit;
 	border-top-right-radius: inherit;
 }

@@ -173,23 +174,24 @@ section > div:last-child {
 	border-bottom: 0;
 }
 
-section > div * {
-	align-self: center !important;
-}
-
-section > div .left {
-	margin-left: auto;
-	font-size: 100%;
-	margin-right: 1em;
+section > .title {
+	color: var(--text-color);
+	padding-left: 1em;
+	background-color: var(--section-header-background-color);
 }
 
 section > div .title{
-	padding: 0.5rem;
+	padding: .5em;
 	white-space: nowrap;
 	overflow: hidden;
 	text-overflow: ellipsis;
 }
 
+section > div .right {
+	margin-left: auto;
+	margin-right: .25em;
+}
+
 
 /* Switch Styling*/
 .switch {

@@ -231,7 +233,7 @@ section > div .title{
 	-webkit-appearance: none;
 	height: 10px;
 	border-radius: 5px;   
-	background: var(--switch-background-color);
+	background: var(--slider-background-color);
 	outline: none;
 }
 

@@ -274,6 +276,7 @@ section > div .title{
 	background-color: var(--button-background-color);
 	color: var(--secondary-text-color);
 	padding: .5em .7em;
+	margin: 0;
 }
 
 .buttons > .active {

@@ -288,8 +291,6 @@ section > div .title{
 	width: 32px;
 	height: 32px;
 	animation: spin 1.5s linear infinite;
-	display: inline-block;
-	vertical-align: middle;
 }
 
 

@@ -310,13 +311,13 @@ section > div .title{
 	100% { filter: brightness(85%); }
 }
 
-@media screen and (max-width: 700px) {
+@media screen and (max-width: 50em) {
 	body {
-		padding-top: 58px;
+		padding-top: 4em;
 	}
 
 	section > div:last-child {
-		border-bottom: 1px solid var(--section-border-color);
+		border-bottom: .1em solid var(--section-border-color);
 	}
 
 	section {

@@ -325,3 +326,11 @@ section > div .title{
 		margin-bottom: 0;
 	}
 }
+
+@media all and (display-mode: standalone) {
+	body {
+		user-select: none;
+		-webkit-user-select: none;
+		padding-bottom: 1em;
+	}
+}+
\ No newline at end of file
diff --git a/src/webui.js b/src/webui.js
@@ -6,6 +6,7 @@ import { html, render } from 'lit-html';
 import { map }          from 'lit-html/directives/map.js';
 import { ifDefined }    from 'lit-html/directives/if-defined.js';
 import { unsafeSVG }    from 'lit-html/directives/unsafe-svg.js';
+import { unsafeHTML }    from 'lit-html/directives/unsafe-html.js';
 
 import { connect }      from "mqtt";
 

@@ -54,11 +55,17 @@ const onMessage = (topic, message) => {
 				console.log(exception);
 				return;
 			}
+		} else {
+			value = message;
 		}
 
 		if (value === null) value = message;
 
 		switch (element.dataset.type) {
+			case 'html':
+				element.innerHTML = value;
+				break;
+
 			case 'text':
 				if (typeof value === 'object') return;
 

@@ -84,7 +91,7 @@ const onMessage = (topic, message) => {
 
 				element.value                 = value;
 				element.dataset.lastMqttValue = value;
-				document.getElementById(element.id + '_loader').classList.remove('loader');
+				element.parentNode.getElementsByTagName('div')[0].classList.remove('loader');
 				break;
 
 			case 'button':

@@ -140,6 +147,8 @@ const renderItem = (itemData) => {
 		itemData.topic.set = null;
 	}
 
+	if (typeof itemData.topic === 'undefined') itemData.topic = {}
+
 	let transform = null;
 
 	if (typeof itemData.transform !== 'undefined') {

@@ -150,15 +159,11 @@ const renderItem = (itemData) => {
 	if (itemData.type !== 'html') {
 		let element = html``;
 
-		if (itemData.type == 'select') {
-			itemData.id = Math.random().toString(36).slice(2);
-		}
-
 		switch (itemData.type) {
 			case 'text':
 				if (typeof itemData.topic === 'undefined') break;
 
-				element = html`<span data-type="${itemData.type}" data-mqtt-topic="${itemData.topic.get}" data-transform="${ifDefined(transform)}">undef.</span>`;
+				element = html`<span data-type="${itemData.type}" data-mqtt-topic="${ifDefined(itemData.topic.get)}" data-transform="${ifDefined(transform)}"></span>`;
 				break;
 
 			case 'switch':

@@ -209,16 +214,17 @@ const renderItem = (itemData) => {
 					if (itemData.topic.set === null) return false
 					const element = event.target;
 
+					element.parentNode.getElementsByTagName('div')[0].classList.add('loader');
+
 					client.publish(itemData.topic.set, transformMessage(element.value, itemData), itemData.mqtt);
 
 					// Reset to last known state
 					element.value = element.dataset.lastMqttValue
-					document.getElementById(element.id + '_loader').classList.add('loader');
 				}
 
 				element = html`
-					<div id="${itemData.id}_loader"></div>
-					<select id="${itemData.id}" @change="${selectChangeHandler}" data-type="${itemData.type}" data-mqtt-topic="${itemData.topic.get}" data-transform="${ifDefined(transform)}">
+					<div></div>
+					<select @change="${selectChangeHandler}" data-type="${itemData.type}" data-mqtt-topic="${itemData.topic.get}" data-transform="${ifDefined(transform)}">
 						${map(itemData.selectOptions, (option) => html`<option value="${option.value}">${option.label}</option>`)}
 					</select>
 				`;

@@ -277,16 +283,16 @@ const renderItem = (itemData) => {
 			<div @click="${ifDefined(clickHandler)}" style="${ifDefined(cursorStyle)}">
 				<img class="icon" src="${ifDefined(itemData.icon)}">
 				<div class="title">${itemData.title}</div>
-				<div class="left">${element}</div>
+				<div class="right">${element}</div>
 			</div>
 		`;
 	} else {
-		return html`<div data-mqtt-topic="${itemData.topic.get}" data-transform="${ifDefined(transform)}">${itemData.html}</div>`;
+		return html`<div data-type="${itemData.type}" data-mqtt-topic="${ifDefined(itemData.topic.get)}" data-transform="${ifDefined(transform)}">${unsafeHTML(itemData.html)}</div>`;
 	}
 }
 
 const renderSection = (sectionConfig) => {
-	const title = (sectionConfig.title) ? html`<div>${sectionConfig.title}</div>` : html``
+	const title = (sectionConfig.title) ? html`<div class="title">${sectionConfig.title}</div>` : html``
 	return html`
 		<section>
 			${title}

@@ -296,14 +302,15 @@ const renderSection = (sectionConfig) => {
 }
 
 const renderPage = (pageConfig) => {
-	const backButton = (pageConfig.id !== 'mainpage') ? html `<button class="icon" onClick="window.history.back();">${unsafeSVG(backButtonSvg)}</button>` : html `<span class="icon"></span>`;
+	const backButton = (pageConfig.id !== 'mainpage')        ? html`<button class="icon" onClick="window.history.back();">${unsafeSVG(backButtonSvg)}</button>` : html `<span class="icon"></span>`;
+	const image      = (typeof pageConfig.icon === 'string') ? html`<img src="${pageConfig.icon}" width="30" height="30">`                                      : html``;
 
 	return  html`
 		<div id="${pageConfig.id}" class="page">
 			<nav>
 				<header>
 					${backButton}
-					<a href="#"><img src="${ifDefined(pageConfig.icon)}" width="30" height="30"> ${pageConfig.title}</a>
+					<span>${image} ${pageConfig.title}</span>
 					<button class="icon connectionStatus" @click="${() => window.location.reload(true)}">${unsafeSVG(disconnectedSvg)}</button>
 				</header>
 			</nav>

@@ -315,11 +322,10 @@ const renderPage = (pageConfig) => {
 const renderPages = (config) => html`${map(config.pages, (pageConfig) => renderPage(pageConfig))}`;
 
 const scrollHandler = (event) => {
-	if (window.scrollY > 0) {
-		document.querySelectorAll('nav').forEach((element) => element.classList.add('scroll'));
-	} else {
-		document.querySelectorAll('nav').forEach((element) => element.classList.remove('scroll'));
-	}
+	let elements = [...document.getElementsByTagName('nav')];
+
+	if (window.scrollY > 0) { elements.forEach((element) => element.classList.add('scroll')); }
+	else                    { elements.forEach((element) => element.classList.remove('scroll')); }
 }
 
 window.addEventListener('hashchange', goToPage);

@@ -397,3 +403,4 @@ window.addEventListener('DOMContentLoaded', async (event) => {
 
 	client.subscribe([...topics])
 });
+	+
\ No newline at end of file