webui: more handlers typings

This commit is contained in:
Maxim Prokhorov
2024-07-02 18:41:29 +03:00
parent 28c748e393
commit 585efd2cba
6 changed files with 164 additions and 89 deletions

View File

@@ -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];
});

View File

@@ -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());

View File

@@ -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);
});
}

View File

@@ -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")

View File

@@ -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

View File

@@ -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) {