UNPKG

homebridge-plugin-utils

Version:

Opinionated utilities to provide common capabilities and create rich configuration webUI experiences for Homebridge plugins.

190 lines (136 loc) 6.07 kB
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved. * * webUi.mjs: Plugin webUI. */ "use strict"; import { webUiFeatureOptions } from "./webUi-featureoptions.mjs"; export class webUi { // Feature options class instance. featureOptions; // First run webUI callback endpoints for customization. #firstRun; // Plugin name. #name; /** * featureOptions - parameters to webUiFeatureOptions. * firstRun - first run handlers: * isRequired - do we need to run the first run UI workflow? * onStart - initialization for the first run webUI to populate forms and other startup tasks. * onSubmit - execute the first run workflow, typically a login or configuration validation of some sort. * name - plugin name. */ constructor({ featureOptions, firstRun = {}, name } = {}) { // Defaults for our first run handlers. this.#firstRun = { isRequired: () => false, onStart: () => true, onSubmit: () => true }; // Figure out the options passed in to us. this.featureOptions = new webUiFeatureOptions(featureOptions); this.#firstRun = Object.assign({}, this.#firstRun, firstRun); this.#name = name; } /** * Render the webUI. */ // Render the UI. show() { // Fire off our UI, catching errors along the way. try { this.#launchWebUI(); } catch(err) { // If we had an error instantiating or updating the UI, notify the user. homebridge.toast.error(err.message, "Error"); } finally { // Always leave the UI in a usable place for the end user. homebridge.hideSpinner(); } } // Show the first run user experience if needed. async #showFirstRun() { const buttonFirstRun = document.getElementById("firstRun"); // Run a custom initialization handler the user may have provided. if(!(await this.#processHandler(this.#firstRun.onStart))) { return; } // We disable saving any settings until we configure the plugin. homebridge.disableSaveButton(); // First run user experience. buttonFirstRun.addEventListener("click", async () => { // Show the beachball while we setup. homebridge.showSpinner(); // Run a custom submit handler the user may have provided. if(!(await this.#processHandler(this.#firstRun.onSubmit))) { return; } // Create our UI and allow users to save the configuration. document.getElementById("pageFirstRun").style.display = "none"; document.getElementById("menuWrapper").style.display = "inline-flex"; await this.featureOptions.show(); homebridge.enableSaveButton(); // All done. Let the user interact with us, although in practice, we shouldn't get here. // homebridge.hideSpinner(); }); document.getElementById("pageFirstRun").style.display = "block"; } // Show the main plugin configuration tab. #showSettings() { // Show the beachball while we setup. homebridge.showSpinner(); this.featureOptions.hide(); // Highlight the tab in our UI. this.#toggleClasses("menuHome", "btn-elegant", "btn-primary"); this.#toggleClasses("menuFeatureOptions", "btn-elegant", "btn-primary"); this.#toggleClasses("menuSettings", "btn-primary", "btn-elegant"); document.getElementById("pageSupport").style.display = "none"; document.getElementById("pageFeatureOptions").style.display = "none"; homebridge.showSchemaForm(); // All done. Let the user interact with us. homebridge.hideSpinner(); } // Show the support tab. #showSupport() { // Show the beachball while we setup. homebridge.showSpinner(); homebridge.hideSchemaForm(); this.featureOptions.hide(); // Highlight the tab in our UI. this.#toggleClasses("menuHome", "btn-primary", "btn-elegant"); this.#toggleClasses("menuFeatureOptions", "btn-elegant", "btn-primary"); this.#toggleClasses("menuSettings", "btn-elegant", "btn-primary"); document.getElementById("pageSupport").style.display = "block"; document.getElementById("pageFeatureOptions").style.display = "none"; // All done. Let the user interact with us. homebridge.hideSpinner(); } // Launch our webUI. async #launchWebUI() { // Retrieve the current plugin configuration. this.featureOptions.currentConfig = await homebridge.getPluginConfig(); // Add our event listeners to animate the UI. document.getElementById("menuHome").addEventListener("click", () => this.#showSupport()); document.getElementById("menuFeatureOptions").addEventListener("click", async () => await this.featureOptions.show()); document.getElementById("menuSettings").addEventListener("click", () => this.#showSettings()); // If we've got devices detected, we launch our feature option UI. Otherwise, we launch our first run UI. if(this.featureOptions.currentConfig.length && !(await this.#processHandler(this.#firstRun.isRequired))) { document.getElementById("menuWrapper").style.display = "inline-flex"; await this.featureOptions.show(); return; } // If we have the name property set for the plugin configuration yet, let's do so now. If we don't have a configuration, let's initialize it as well. (this.featureOptions.currentConfig[0] ??= { name: this.#name }).name ??= this.#name; // Update the plugin configuration and launch the first run UI. await homebridge.updatePluginConfig(this.featureOptions.currentConfig); this.#showFirstRun(); } // Utility to process user-provided custom handlers that can handle both synchronous and asynchronous handlers. async #processHandler(handler) { if(((typeof handler === "function") && !(await handler())) || ((typeof handler !== "function") && !handler)) { return false; } return true; } // Utility to toggle our classes. #toggleClasses(id, removeClass, addClass) { const element = document.getElementById(id); element.classList.remove(removeClass); element.classList.add(addClass); } }