diff --git a/code/html/src/core.mjs b/code/html/src/core.mjs
index fbc8434a..7a36c3f7 100644
--- a/code/html/src/core.mjs
+++ b/code/html/src/core.mjs
@@ -1,3 +1,6 @@
+/**
+ * @param {string[]} rules
+ */
export function styleInject(rules) {
if (!rules.length) {
return;
@@ -7,23 +10,30 @@ export function styleInject(rules) {
style.setAttribute("type", "text/css");
document.head.appendChild(style);
- let pos = style.sheet.cssRules.length;
+ const sheet = style.sheet;
+ if (!sheet) {
+ return;
+ }
+
+ let pos = sheet.cssRules.length;
for (let rule of rules) {
style.sheet.insertRule(rule, pos++);
}
}
+/**
+ * @param {string} selector
+ * @param {boolean} value
+ */
export function styleVisible(selector, value) {
return `${selector} { content-visibility: ${value ? "visible": "hidden"}; }`
}
/**
- * @param {number} ms
+ * @param {number} timeout
*/
-export function pageReloadIn(ms) {
- setTimeout(() => {
- window.location.reload();
- }, parseInt(ms, 10));
+export function pageReloadIn(timeout) {
+ setTimeout(window.location.reload, timeout);
}
/**
@@ -41,11 +51,9 @@ export function moreElem(container) {
});
}
-export function toggleMenu(event) {
- event.preventDefault();
- event.target.parentElement.classList.toggle("active");
-}
-
+/**
+ * @param {string} name
+ */
export function showPanelByName(name) {
// only a single panel is shown on the 'layout'
const target = document.getElementById(`panel-${name}`);
@@ -53,13 +61,20 @@ export function showPanelByName(name) {
return;
}
- for (const panel of document.querySelectorAll(".panel")) {
+ for (const panel of document.getElementsByClassName("panel")) {
+ if (!(panel instanceof HTMLElement)) {
+ continue;
+ }
+
panel.style.display = "none";
}
+
target.style.display = "revert";
const layout = document.getElementById("layout");
- layout.classList.remove("active");
+ if (layout) {
+ layout.classList.remove("active");
+ }
// TODO: sometimes, switching view causes us to scroll past
// the header (e.g. emon ratios panel on small screen)
@@ -70,32 +85,43 @@ export function showPanelByName(name) {
}
}
-export function showPanel(event) {
+/**
+ * @param {Event} event
+ */
+export function onPanelTargetClick(event) {
event.preventDefault();
- showPanelByName(event.target.dataset["panel"]);
-}
-export function randomString(length, args) {
- if (typeof args === "undefined") {
- args = {
- lowercase: true,
- uppercase: true,
- numbers: true,
- special: true
- }
+ const target = event.target;
+ if (!(target instanceof HTMLElement)) {
+ return;
}
+ const name = target.dataset["panel"];
+ if (name) {
+ showPanelByName(name);
+ }
+}
+
+/**
+ * @typedef {{hex?: boolean, lowercase?: boolean, numbers?: boolean, special?: boolean, uppercase?: boolean}} RandomStringOptions
+ *
+ * @param {number} length
+ * @param {RandomStringOptions} options
+ */
+export function randomString(length, {hex = false, lowercase = true, numbers = true, special = false, uppercase = true} = {}) {
let mask = "";
- if (args.lowercase) { mask += "abcdefghijklmnopqrstuvwxyz"; }
- if (args.uppercase) { mask += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; }
- if (args.numbers || args.hex) { mask += "0123456789"; }
- if (args.hex) { mask += "ABCDEF"; }
- if (args.special) { mask += "~`!@#$%^&*()_+-={}[]:\";'<>?,./|\\"; }
+ if (lowercase || hex) { mask += "abcdef"; }
+ if (lowercase) { mask += "ghijklmnopqrstuvwxyz"; }
+ if (uppercase || hex) { mask += "ABCDEF"; }
+ if (uppercase) { mask += "GHIJKLMNOPQRSTUVWXYZ"; }
+ if (numbers || hex) { mask += "0123456789"; }
+ if (special) { mask += "~`!@#$%^&*()_+-={}[]:\";'<>?,./|\\"; }
- let source = new Uint32Array(length);
- let result = new Array(length);
+ const source = new Uint32Array(length);
+ const result = new Array(length);
- window.crypto.getRandomValues(source)
+ window.crypto
+ .getRandomValues(source)
.forEach((value, i) => {
result[i] = mask[value % mask.length];
});
diff --git a/code/html/src/index.mjs b/code/html/src/index.mjs
index e5d7320f..44e16869 100644
--- a/code/html/src/index.mjs
+++ b/code/html/src/index.mjs
@@ -6,21 +6,18 @@ window.onerror = notifyError;
import {
pageReloadIn,
randomString,
- showPanel,
+ onPanelTargetClick,
styleInject,
} from './core.mjs';
import { validatePassword, validateFormsPasswords } from './validate.mjs';
-import {
- askAndCallAction,
- askAndCallReboot,
- askAndCallReconnect,
-} from './question.mjs';
+import { askAndCall } from './question.mjs';
import {
init as initSettings,
applySettings,
+ askSaveSettings,
getData,
setChangedElement,
updateVariables,
@@ -29,7 +26,11 @@ import {
import { init as initWiFi } from './wifi.mjs';
import { init as initGpio } from './gpio.mjs';
-import { init as initConnection, connect } from './connection.mjs';
+import {
+ init as initConnection,
+ connect,
+ sendAction,
+} from './connection.mjs';
import { init as initApi } from './api.mjs';
import { init as initCurtain } from './curtain.mjs';
@@ -221,8 +222,52 @@ function keepTime() {
}
}
+/** @import { QuestionWrapper } from './question.mjs' */
+
+/** @type {QuestionWrapper} */
+function askDisconnect(ask) {
+ return ask("Are you sure you want to disconnect from the current WiFi network?");
+}
+
+/** @type {QuestionWrapper} */
+function askReboot(ask) {
+ return ask("Are you sure you want to reboot the device?");
+}
+
+function askAndCallReconnect() {
+ askAndCall([askSaveSettings, askDisconnect], () => {
+ sendAction("reconnect");
+ });
+}
+
+function askAndCallReboot() {
+ askAndCall([askSaveSettings, askReboot], () => {
+ sendAction("reboot");
+ });
+}
+
+/** @param {Event} event */
+function askAndCallSimpleAction(event) {
+ const target = event.target;
+ if (!(target instanceof HTMLButtonElement)) {
+ return;
+ }
+
+ /** @type {QuestionWrapper} */
+ const wrapper =
+ (ask) => ask(`Confirm the action: "${target.textContent}"`);
+
+ askAndCall([wrapper], () => {
+ sendAction(target.name);
+ });
+}
+
/**
- * @returns {import("./settings.mjs").KeyValueListeners}
+ * @import { KeyValueListeners } from './settings.mjs'
+ */
+
+/**
+ * @returns {KeyValueListeners}
*/
function listeners() {
return {
@@ -298,17 +343,32 @@ function initSetupPassword(form) {
* @param {Event} event
* @returns {any}
*/
-function toggleMenu(event) {
+function onMenuLinkClick(event) {
event.preventDefault();
- /** @type {HTMLElement} */(event.target).parentElement?.classList.toggle("active");
+
+ const target = event.target;
+ if (!(target instanceof HTMLElement)) {
+ return;
+ }
+
+ if (target?.parentElement) {
+ target.parentElement.classList.toggle("active");
+ }
}
/**
* @param {Event} event
*/
-function toggleVisiblePassword(event) {
- const target = /** @type {HTMLSpanElement} */(event.target);
- const input = /** @type {HTMLInputElement} */(target.previousElementSibling);
+function onPasswordRevealClick(event) {
+ const target = event.target;
+ if (!(target instanceof HTMLElement)) {
+ return;
+ }
+
+ const input = target.previousElementSibling;
+ if (!(input instanceof HTMLInputElement)) {
+ return;
+ }
if (input.type === "password") {
input.type = "text";
@@ -348,15 +408,15 @@ function init() {
document.querySelectorAll(".password-reveal")
.forEach((elem) => {
- elem.addEventListener("click", toggleVisiblePassword);
+ elem.addEventListener("click", onPasswordRevealClick);
});
// Sidebar menu & buttons
document.querySelector(".menu-link")
- ?.addEventListener("click", toggleMenu);
+ ?.addEventListener("click", onMenuLinkClick);
document.querySelectorAll(".pure-menu-link")
.forEach((elem) => {
- elem.addEventListener("click", showPanel);
+ elem.addEventListener("click", onPanelTargetClick);
});
document.querySelector(".button-reconnect")
@@ -369,7 +429,7 @@ function init() {
// Generic action sender
document.querySelectorAll(".button-simple-action")
.forEach((elem) => {
- elem.addEventListener("click", askAndCallAction);
+ elem.addEventListener("click", askAndCallSimpleAction);
});
variableListeners(listeners());
diff --git a/code/html/src/question.mjs b/code/html/src/question.mjs
index d534abd8..90fb1179 100644
--- a/code/html/src/question.mjs
+++ b/code/html/src/question.mjs
@@ -1,22 +1,15 @@
-import { pendingChanges } from './settings.mjs';
-import { sendAction } from './connection.mjs';
+/**
+ * @typedef {function(string): boolean} Question
+ */
-export function askSaveSettings(ask) {
- if (pendingChanges()) {
- return ask("There are pending changes to the settings, continue the operation without saving?");
- }
-
- return true;
-}
-
-export function askDisconnect(ask) {
- return ask("Are you sure you want to disconnect from the current WiFi network?");
-}
-
-export function askReboot(ask) {
- return ask("Are you sure you want to reboot the device?");
-}
+/**
+ * @typedef {function(Question): boolean} QuestionWrapper
+ */
+/**
+ * @param {QuestionWrapper[]} questions
+ * @param {function(): void} callback
+ */
export function askAndCall(questions, callback) {
for (let question of questions) {
if (!question(window.confirm)) {
@@ -26,22 +19,3 @@ export function askAndCall(questions, callback) {
callback();
}
-
-export function askAndCallReconnect() {
- askAndCall([askSaveSettings, askDisconnect], () => {
- sendAction("reconnect");
- });
-}
-
-export function askAndCallReboot() {
- askAndCall([askSaveSettings, askReboot], () => {
- sendAction("reboot");
- });
-}
-
-export function askAndCallAction(event) {
- askAndCall([(ask) => ask(`Confirm the action: "${event.target.textContent}"`)], () => {
- sendAction(event.target.name);
- });
-}
-
diff --git a/code/html/src/sensor.mjs b/code/html/src/sensor.mjs
index 96db50e6..70263bc5 100644
--- a/code/html/src/sensor.mjs
+++ b/code/html/src/sensor.mjs
@@ -8,7 +8,7 @@ import {
} from './template.mjs';
import {
- showPanel,
+ onPanelTargetClick,
showPanelByName,
styleInject,
styleVisible,
@@ -358,7 +358,7 @@ export function init() {
variableListeners(listeners());
document.querySelector(".button-emon-expected")
- .addEventListener("click", showPanel);
+ .addEventListener("click", onPanelTargetClick);
document.querySelector(".button-emon-expected-calculate")
.addEventListener("click", emonCalculateRatios);
document.querySelector(".button-emon-expected-apply")
diff --git a/code/html/src/settings.mjs b/code/html/src/settings.mjs
index ac03271a..c35b622f 100644
--- a/code/html/src/settings.mjs
+++ b/code/html/src/settings.mjs
@@ -1195,6 +1195,20 @@ export function pendingChanges() {
return Settings.counters.changed > 0;
}
+/**
+ * TODO https://github.com/microsoft/TypeScript/issues/58969 ? at-import becomes 'unused' for some reason
+ * @typedef {import("./question.mjs").QuestionWrapper} QuestionWrapper
+ */
+
+/** @type {QuestionWrapper} */
+export function askSaveSettings(ask) {
+ if (pendingChanges()) {
+ return ask("There are pending changes to the settings, continue the operation without saving?");
+ }
+
+ return true;
+}
+
/** @returns {KeyValueListeners} */
function listeners() {
return {
@@ -1249,7 +1263,7 @@ export function init() {
document.querySelectorAll(".button-add-settings-group")
.forEach((elem) => {
- elem.addEventListener("click", groupSettingsAdd);
+ elem.addEventListener("click", onGroupSettingsAddClick);
});
// No group handler should be registered after this point, since we depend on the order
diff --git a/code/html/src/thermostat.mjs b/code/html/src/thermostat.mjs
index 62c4186b..c678d2e9 100644
--- a/code/html/src/thermostat.mjs
+++ b/code/html/src/thermostat.mjs
@@ -1,4 +1,5 @@
-import { askAndCall, askSaveSettings } from './question.mjs';
+import { askAndCall } from './question.mjs';
+import { askSaveSettings } from './settings.mjs';
import { sendAction } from './connection.mjs';
function checkTempRange(event) {