From 585efd2cbae190cae839dc99eb00dc3b574e64b6 Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Tue, 2 Jul 2024 18:41:29 +0300 Subject: [PATCH] webui: more handlers typings --- code/html/src/core.mjs | 90 ++++++++++++++++++++++------------ code/html/src/index.mjs | 94 +++++++++++++++++++++++++++++------- code/html/src/question.mjs | 46 ++++-------------- code/html/src/sensor.mjs | 4 +- code/html/src/settings.mjs | 16 +++++- code/html/src/thermostat.mjs | 3 +- 6 files changed, 164 insertions(+), 89 deletions(-) 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) {