homebridge-ratgdo 2.11.0 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -156,6 +156,9 @@ import { FeatureOptions} from "./featureoptions.js";
156
156
  */
157
157
  export class webUiFeatureOptions {
158
158
 
159
+ // Map of category UI state indexed by context key (serial number or "global").
160
+ #categoryStates;
161
+
159
162
  // Table containing the currently displayed feature options.
160
163
  #configTable;
161
164
 
@@ -241,6 +244,7 @@ export class webUiFeatureOptions {
241
244
  } = options;
242
245
 
243
246
  // Initialize all our properties. We cache DOM elements for performance and maintain state for the current controller and device context.
247
+ this.#categoryStates = {};
244
248
  this.#configTable = document.getElementById("configTable");
245
249
  this.#controller = null;
246
250
  this.#controllersContainer = document.getElementById("controllersContainer");
@@ -476,19 +480,18 @@ export class webUiFeatureOptions {
476
480
 
477
481
  const isCollapsed = tbody.style.display === "none";
478
482
 
479
- tbody.style.display = isCollapsed ? "" : "none";
480
-
481
- const arrow = table.querySelector(".arrow");
483
+ // Use the shared method to update the state.
484
+ this.#setCategoryState(table, !isCollapsed);
482
485
 
483
- if(arrow) {
486
+ document.getElementById("toggleAllCategories")?.updateState?.();
484
487
 
485
- arrow.textContent = isCollapsed ? "\u25BC " : "\u25B6 ";
486
- }
488
+ // Save the state after toggling this category.
489
+ const currentContext = this.#getCurrentContextKey();
487
490
 
488
- // Update accessibility state to reflect the current expansion state for assistive technologies.
489
- headerCell.setAttribute("aria-expanded", isCollapsed ? "true" : "false");
491
+ if(currentContext) {
490
492
 
491
- document.getElementById("toggleAllCategories")?.updateState?.();
493
+ this.#saveCategoryStates(currentContext);
494
+ }
492
495
  }
493
496
 
494
497
  return;
@@ -689,6 +692,35 @@ export class webUiFeatureOptions {
689
692
  }
690
693
  }
691
694
 
695
+ /**
696
+ * Set the expansion state of a category table.
697
+ *
698
+ * @param {HTMLTableElement} table - The category table element.
699
+ * @param {boolean} isCollapsed - True to collapse, false to expand.
700
+ * @private
701
+ */
702
+ #setCategoryState(table, isCollapsed) {
703
+
704
+ const tbody = table.querySelector("tbody");
705
+ const arrow = table.querySelector(".arrow");
706
+ const headerCell = table.querySelector("thead th[role='button']");
707
+
708
+ if(tbody) {
709
+
710
+ tbody.style.display = isCollapsed ? "none" : "";
711
+ }
712
+
713
+ if(arrow) {
714
+
715
+ arrow.textContent = isCollapsed ? "\u25B6 " : "\u25BC ";
716
+ }
717
+
718
+ if(headerCell) {
719
+
720
+ headerCell.setAttribute("aria-expanded", isCollapsed ? "false" : "true");
721
+ }
722
+ }
723
+
692
724
  /**
693
725
  * Hide the feature options webUI and clean up all resources.
694
726
  *
@@ -724,6 +756,13 @@ export class webUiFeatureOptions {
724
756
  */
725
757
  #showGlobalOptions() {
726
758
 
759
+ // Save the current UI state before switching contexts.
760
+ const previousContext = this.#getCurrentContextKey();
761
+
762
+ if(previousContext && this.#configTable.querySelector("table[data-category]")) {
763
+
764
+ this.#saveCategoryStates(previousContext);
765
+ }
727
766
 
728
767
  // Clear the devices container since global options don't have associated devices, but only when we have controllers defined.
729
768
  if(this.#getControllers) {
@@ -748,6 +787,14 @@ export class webUiFeatureOptions {
748
787
  */
749
788
  async #showControllerOptions(controllerSerial) {
750
789
 
790
+ // Save the current UI state before switching contexts.
791
+ const previousContext = this.#getCurrentContextKey();
792
+
793
+ if(previousContext && this.#configTable.querySelector("table[data-category]")) {
794
+
795
+ this.#saveCategoryStates(previousContext);
796
+ }
797
+
751
798
  const entry = (await this.#getControllers())?.find(c => c.serialNumber === controllerSerial);
752
799
 
753
800
  if(!entry) {
@@ -786,6 +833,23 @@ export class webUiFeatureOptions {
786
833
  // Make sure we have the refreshed configuration. This ensures we're always working with the latest saved settings.
787
834
  this.currentConfig = await homebridge.getPluginConfig();
788
835
 
836
+ // Load any persisted UI states from localStorage.
837
+ try {
838
+
839
+ const storageKey = "homebridge-" + (this.currentConfig[0]?.platform ?? "plugin") + "-category-states";
840
+ const stored = window.localStorage.getItem(storageKey);
841
+
842
+ if(stored) {
843
+
844
+ this.#categoryStates = JSON.parse(stored);
845
+ }
846
+
847
+ // eslint-disable-next-line no-unused-vars
848
+ } catch(error) {
849
+
850
+ this.#categoryStates = {};
851
+ }
852
+
789
853
  // Keep our revert snapshot aligned with whatever was *last saved* (not just first render).
790
854
  // We compare to the current config and update the snapshot if it differs, so "Revert to Saved" reflects the latest saved state.
791
855
  const loadedOptions = (this.currentConfig[0]?.options ?? []);
@@ -1279,6 +1343,15 @@ export class webUiFeatureOptions {
1279
1343
 
1280
1344
  homebridge.showSpinner();
1281
1345
 
1346
+ // Retrieve our current context before we change the active link.
1347
+ const previousActive = this.#devicesContainer.querySelector(".nav-link.active[data-navigation='device']");
1348
+
1349
+ // Save the current category UI state before switching contexts.
1350
+ if(previousActive && (previousActive.name !== deviceId) && this.#configTable.querySelector("table[data-category]")) {
1351
+
1352
+ this.#saveCategoryStates(previousActive.name);
1353
+ }
1354
+
1282
1355
  // Clean up event listeners from previous option displays. This ensures we don't accumulate listeners as users navigate between devices.
1283
1356
  this.#cleanupOptionEventListeners();
1284
1357
 
@@ -1299,6 +1372,9 @@ export class webUiFeatureOptions {
1299
1372
  // Create option tables for each category. Categories group related options together for better organization.
1300
1373
  this.#createOptionTables(currentDevice);
1301
1374
 
1375
+ // Restore saved category UI state context for this device.
1376
+ this.#restoreCategoryStates(deviceId);
1377
+
1302
1378
  // Set up search functionality if available. This includes debounced search and keyboard shortcuts.
1303
1379
  this.#setupSearchFunctionality();
1304
1380
 
@@ -1807,6 +1883,14 @@ export class webUiFeatureOptions {
1807
1883
  }
1808
1884
 
1809
1885
  toggleBtn.updateState();
1886
+
1887
+ // Save the category UI state after toggling all categories.
1888
+ const currentContext = this.#getCurrentContextKey();
1889
+
1890
+ if(currentContext) {
1891
+
1892
+ this.#saveCategoryStates(currentContext);
1893
+ }
1810
1894
  }
1811
1895
 
1812
1896
  /**
@@ -1853,6 +1937,7 @@ export class webUiFeatureOptions {
1853
1937
  // Create a unique id for the tbody so that the header can reference it for accessibility.
1854
1938
  const tbodyId = "tbody-" + category.name.replace(/\s+/g, "-");
1855
1939
 
1940
+ // We default category tables to collapsed.
1856
1941
  const tbody = this.#createElement("tbody", {
1857
1942
 
1858
1943
  classList: [ "border", "category-border" ],
@@ -1933,6 +2018,7 @@ export class webUiFeatureOptions {
1933
2018
 
1934
2019
  const scopeLabel = !currentDevice ? " (Global)" : (this.#ui.isController(currentDevice) ? " (Controller-specific)" : " (Device-specific)");
1935
2020
 
2021
+ // We default category tables to collapsed.
1936
2022
  const th = this.#createElement("th", {
1937
2023
 
1938
2024
  "aria-controls": tbodyId,
@@ -2202,6 +2288,98 @@ export class webUiFeatureOptions {
2202
2288
  return label;
2203
2289
  }
2204
2290
 
2291
+ /**
2292
+ * Save the current category UI state for the given context.
2293
+ *
2294
+ * @param {string} contextKey - The context identifier (device serial or "Global Options").
2295
+ * @private
2296
+ */
2297
+ #saveCategoryStates(contextKey) {
2298
+
2299
+ const states = {};
2300
+ const tables = document.querySelectorAll("#configTable table[data-category]");
2301
+
2302
+ for(const table of tables) {
2303
+
2304
+ const categoryName = table.getAttribute("data-category");
2305
+ const tbody = table.querySelector("tbody");
2306
+
2307
+ if(tbody) {
2308
+
2309
+ // Store true if collapsed (display: none), false if expanded.
2310
+ states[categoryName] = tbody.style.display === "none";
2311
+ }
2312
+ }
2313
+
2314
+ this.#categoryStates[contextKey] = states;
2315
+
2316
+ // Finally, we persist to localStorage to remember user preferences.
2317
+ try {
2318
+
2319
+ const storageKey = "homebridge-" + (this.currentConfig[0]?.platform ?? "plugin") + "-category-states";
2320
+
2321
+ window.localStorage.setItem(storageKey, JSON.stringify(this.#categoryStates));
2322
+
2323
+ // eslint-disable-next-line no-empty, no-unused-vars
2324
+ } catch(error) {
2325
+
2326
+ }
2327
+ }
2328
+
2329
+ /**
2330
+ * Restore saved category UI state for the given context.
2331
+ *
2332
+ * @param {string} contextKey - The context identifier (device serial or "Global Options").
2333
+ * @private
2334
+ */
2335
+ #restoreCategoryStates(contextKey) {
2336
+
2337
+ const savedStates = this.#categoryStates[contextKey];
2338
+
2339
+ if(!savedStates) {
2340
+
2341
+ return;
2342
+ }
2343
+
2344
+ const tables = document.querySelectorAll("#configTable table[data-category]");
2345
+
2346
+ for(const table of tables) {
2347
+
2348
+ const categoryName = table.getAttribute("data-category");
2349
+
2350
+ if(categoryName in savedStates) {
2351
+
2352
+ this.#setCategoryState(table, savedStates[categoryName]);
2353
+ }
2354
+ }
2355
+
2356
+ // Update the toggle button state after restoring.
2357
+ document.getElementById("toggleAllCategories")?.updateState?.();
2358
+ }
2359
+
2360
+ /**
2361
+ * Get the current context key for UI state tracking.
2362
+ *
2363
+ * @returns {string} The context key for the current view.
2364
+ * @private
2365
+ */
2366
+ #getCurrentContextKey() {
2367
+
2368
+ // Check devices first - if there's an active device, that's our context.
2369
+ const activeDevice = this.#devicesContainer.querySelector(".nav-link.active[data-navigation='device']");
2370
+
2371
+ if(activeDevice) {
2372
+
2373
+ return activeDevice.name;
2374
+ }
2375
+
2376
+ // No active device, so check for either the active controller or global context.
2377
+ const activeController = this.#controllersContainer.querySelector(".nav-link.active[data-navigation]");
2378
+
2379
+ // Default to our global options if we don't have a current controller.
2380
+ return (activeController?.name !== "Global Options") ? this.#devices[0].serialNumber : "Global Options";
2381
+ }
2382
+
2205
2383
  /**
2206
2384
  * Handle option state changes with full hierarchy and dependency management.
2207
2385
  *
@@ -3509,6 +3687,7 @@ export class webUiFeatureOptions {
3509
3687
  */
3510
3688
  cleanup() {
3511
3689
 
3690
+ this.#categoryStates = {};
3512
3691
  this.#cleanupEventListeners();
3513
3692
  this.#eventListeners.clear();
3514
3693
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-ratgdo",
3
3
  "displayName": "Ratgdo",
4
- "version": "2.11.0",
4
+ "version": "2.11.1",
5
5
  "description": "HomeKit integration using Ratgdo and Konnected devices for LiftMaster and Chamberlain garage door openers, without requiring myQ.",
6
6
  "license": "ISC",
7
7
  "repository": {
@@ -41,18 +41,18 @@
41
41
  "ratgdo"
42
42
  ],
43
43
  "devDependencies": {
44
- "@stylistic/eslint-plugin": "5.2.3",
44
+ "@stylistic/eslint-plugin": "5.3.1",
45
45
  "@types/node": "24.3.0",
46
46
  "eslint": "9.34.0",
47
47
  "homebridge": "1.11.0",
48
48
  "shx": "0.4.0",
49
49
  "typescript": "5.9.2",
50
- "typescript-eslint": "8.40.0"
50
+ "typescript-eslint": "8.41.0"
51
51
  },
52
52
  "dependencies": {
53
53
  "@homebridge/plugin-ui-utils": "2.1.0",
54
54
  "bonjour-service": "1.3.0",
55
- "esphome-client": "1.1.1",
56
- "homebridge-plugin-utils": "1.28.0"
55
+ "esphome-client": "1.1.2",
56
+ "homebridge-plugin-utils": "1.29.1"
57
57
  }
58
58
  }