doc: sync plan options-panel code with the shipped fixes

Records the doctype, the render() activeElement guard, and the blur in the
Restore handler. storage.onChanged fires for this page's own writes, so
every autosave re-entered the listener and could overwrite in-progress
typing; the guard closes that.
This commit is contained in:
golem 2026-08-01 22:51:15 -06:00
parent 5e56503244
commit 5548216645

@ -843,6 +843,7 @@ The panel renders **inline inside `about:addons`**, which is a narrow, constrain
The element `id`s must match the keys in `DEFAULT_SETTINGS` exactly — `options.js` relies on that. The element `id`s must match the keys in `DEFAULT_SETTINGS` exactly — `options.js` relies on that.
```html ```html
<!doctype html>
<!-- Rendered inline inside about:addons. Keep it narrow-friendly and do not <!-- Rendered inline inside about:addons. Keep it narrow-friendly and do not
rely on window sizing; a pref can degrade this to a standalone tab. --> rely on window sizing; a pref can degrade this to a standalone tab. -->
<meta charset="utf-8" /> <meta charset="utf-8" />
@ -1054,13 +1055,25 @@ let statusTimer = null;
// values into the form, so a remote sync cannot trigger a save loop. // values into the form, so a remote sync cannot trigger a save loop.
let applying = false; let applying = false;
// Corrected during execution: the activeElement guard is required because
// storage.onChanged fires for this page's OWN writes, so every debounced
// autosave re-entered the listener below and could overwrite whatever the user
// had typed since the save started.
function render(settings) { function render(settings) {
applying = true; applying = true;
field("model").value = settings.model; for (const [id, value] of Object.entries(settings)) {
field("temporaryChat").checked = settings.temporaryChat; const element = field(id);
field("webSearch").checked = settings.webSearch; // Never overwrite the control the user is actively editing — this page's
field("openIn").value = settings.openIn; // own saves echo back through storage.onChanged (see below).
field("promptTemplate").value = settings.promptTemplate; if (element === document.activeElement) {
continue;
}
if (typeof value === "boolean") {
element.checked = value;
} else {
element.value = value;
}
}
applying = false; applying = false;
} }
@ -1111,12 +1124,18 @@ for (const id of Object.keys(DEFAULT_SETTINGS)) {
field("restore").addEventListener("click", () => { field("restore").addEventListener("click", () => {
clearTimeout(saveTimer); clearTimeout(saveTimer);
// Restoring defaults is an explicit user action, so it should reset every
// field even if one currently has focus. Blur first so render()'s
// activeElement guard (see below) has nothing to skip.
document.activeElement?.blur();
render(DEFAULT_SETTINGS); render(DEFAULT_SETTINGS);
persist({ ...DEFAULT_SETTINGS }); persist({ ...DEFAULT_SETTINGS });
}); });
// A remote sync landing while the panel is open must update the controls rather // browser.storage.onChanged fires for every write to the "sync" area, not just
// than being clobbered by stale field values on the next edit. // a remote sync landing on another device — this page's own debounced
// autosaves and its Restore-defaults save re-enter this listener too, which
// render()'s activeElement guard makes safe to react to unconditionally.
browser.storage.onChanged.addListener(async (changes, area) => { browser.storage.onChanged.addListener(async (changes, area) => {
if (area !== "sync") { if (area !== "sync") {
return; return;