@vacantthinker/firefox-addon-framework-easy 2026.605.1200 → 2026.606.751

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.
package/README.md CHANGED
@@ -91,7 +91,7 @@ export class DomainORM extends BaseORM { }
91
91
 
92
92
  ### 📄 File: `src/generate.js`
93
93
  ```javascript
94
- export function generateHtmlByUserSettings(
94
+ export async function generateHtmlByUserSettings(
95
95
  userSettings,
96
96
  radioItemClickCallback
97
97
  ) { }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vacantthinker/firefox-addon-framework-easy",
3
- "version": "2026.0605.1200",
3
+ "version": "2026.0606.0751",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "publishConfig": {
@@ -10,11 +10,14 @@ export async function browserDownloadByDownlink(
10
10
  downlink,
11
11
  filename = null,
12
12
  }) {
13
-
14
- let url = downlink;
15
- let options = {url};
16
- if (filename) {
17
- Object.assign(options, {filename});
18
- }
19
- await browser.downloads.download(options);
20
- }
13
+ let url = downlink;
14
+ /** @type {browser.downloads._DownloadOptions} */
15
+ let options = {
16
+ url,
17
+ saveAs: false
18
+ };
19
+ if (filename) {
20
+ Object.assign(options, { filename });
21
+ }
22
+ await browser.downloads.download(options);
23
+ }
package/src/generate.js CHANGED
@@ -1,128 +1,130 @@
1
1
  import { stoOpGet, stoOpSet } from "./opStorage.js";
2
2
 
3
+ // [Optimization 1] Introduce a debounce function to prevent triggering storage writes on every keystroke.
4
+ function debounce(func, wait) {
5
+ let timeout;
6
+ return function (...args) {
7
+ clearTimeout(timeout);
8
+ timeout = setTimeout(() => func.apply(this, args), wait);
9
+ };
10
+ }
11
+
3
12
  /**
4
13
  * Generates HTML elements based on a user settings schema object.
5
14
  *
6
- * @param {Object} userSettings
7
- * @param {Function} radioItemClickCallback
8
- * @returns {HTMLFieldSetElement[]}
15
+ * @param {Object} userSettings
16
+ * @param {Function} radioItemClickCallback
17
+ * @returns {Promise<DocumentFragment>}
9
18
  */
10
- export function generateHtmlByUserSettings(
19
+ export async function generateHtmlByUserSettings(
11
20
  userSettings,
12
21
  radioItemClickCallback
13
22
  ) {
14
- // Keeps track of all generated fieldsets by their storageKey
15
23
  const elementsMap = {};
16
24
 
17
- const fieldsets = Object.keys(userSettings).map((storageKey) => {
18
- const storageValue = userSettings[storageKey];
19
- const type = storageValue.type || "text"; // Default to text if type is not specified
25
+ // [Optimization 2] Use a DocumentFragment for off-screen DOM construction to improve rendering performance.
26
+ const fragment = document.createDocumentFragment();
27
+
28
+ const keys = Object.keys(userSettings);
29
+
30
+ // [Optimization 3] Batch fetch all stored values.
31
+ // Wait for all queries to complete to prevent async callbacks from interrupting the rendering pipeline.
32
+ const valuesArray = await Promise.all(keys.map((key) => stoOpGet(key)));
33
+ const storageData = {};
34
+ keys.forEach((key, index) => {
35
+ storageData[key] = valuesArray[index];
36
+ });
20
37
 
21
- // Common container wrapper for every configuration item
38
+ // First pass: Pre-create all container wrappers.
39
+ // This ensures all target elements exist in the DOM references before executing triggerVisibility later.
40
+ keys.forEach((storageKey) => {
22
41
  const eleWrap = document.createElement("fieldset");
23
42
  const eleTitle = document.createElement("legend");
24
43
  eleTitle.textContent = storageKey;
25
44
  eleWrap.append(eleTitle);
26
45
 
27
- // Save a reference to the wrapper element for visibility switching
28
46
  elementsMap[storageKey] = eleWrap;
47
+ fragment.append(eleWrap);
48
+ });
49
+
50
+ // Second pass: Synchronously populate form content and attach event listeners.
51
+ keys.forEach((storageKey) => {
52
+ const storageValue = userSettings[storageKey];
53
+ const type = storageValue.type || "text";
54
+ const eleWrap = elementsMap[storageKey];
55
+
56
+ // Retrieve pre-loaded value; fallback to the schema's default value if undefined.
57
+ const storedValue = storageData[storageKey];
58
+ const initialValue =
59
+ storedValue !== undefined && storedValue !== null
60
+ ? storedValue
61
+ : storageValue.selected;
29
62
 
30
63
  // --- CONDITION 1: CHECKBOX & RADIO ---
31
64
  if (type === "checkbox" || type === "radio") {
32
65
  /** @type {string[]} */
33
66
  const options = storageValue.options || [];
34
67
 
35
- options
36
- .map((option) => {
37
- const eleLabel = document.createElement("label");
38
- eleLabel.textContent = option;
39
-
40
- const eleInput = document.createElement("input");
41
- eleInput.name = storageKey;
42
- eleInput.type = type;
43
- eleInput.value = option;
44
-
45
- if (type === "checkbox") {
46
- stoOpGet(storageKey).then((v) => {
47
- const initialArray = Array.from(v || storageValue.selected || []);
48
- const set = new Set(initialArray);
49
- eleInput.checked = set.has(option);
50
-
51
- // Initial visibility evaluation
52
- triggerVisibility(storageKey, initialArray);
53
- });
54
-
55
- eleInput.addEventListener("change", async () => {
56
- const optionsCurrent =
57
- (await stoOpGet(storageKey)) || storageValue.selected || [];
58
- const set = new Set(Array.from(optionsCurrent));
59
-
60
- if (eleInput.checked) {
61
- set.add(option);
62
- } else {
63
- set.delete(option);
64
- }
65
-
66
- const valueNew = Array.from(set);
67
- await stoOpSet(storageKey, valueNew);
68
+ options.forEach((option) => {
69
+ const eleLabel = document.createElement("label");
70
+ eleLabel.textContent = option;
71
+ const eleInput = document.createElement("input");
72
+ eleInput.name = storageKey;
73
+ eleInput.type = type;
74
+ eleInput.value = option;
75
+
76
+ if (type === "checkbox") {
77
+ const initialArray = Array.from(initialValue || []);
78
+ const set = new Set(initialArray);
79
+ eleInput.checked = set.has(option); // Apply state synchronously
80
+
81
+ eleInput.addEventListener("change", async () => {
82
+ // Derive state in real-time from the DOM (faster than reading from storage again)
83
+ const allCheckboxes = eleWrap.querySelectorAll(
84
+ 'input[type="checkbox"]'
85
+ );
86
+ const valueNew = Array.from(allCheckboxes)
87
+ .filter((cb) => cb.checked)
88
+ .map((cb) => cb.value);
89
+
90
+ await stoOpSet(storageKey, valueNew);
91
+ triggerVisibility(storageKey, valueNew);
92
+ });
93
+ } else if (type === "radio") {
94
+ if (option === initialValue) {
95
+ eleInput.checked = true; // Apply state synchronously
96
+ }
68
97
 
69
- // Dynamic visibility update
70
- triggerVisibility(storageKey, valueNew);
71
- });
72
- } else if (type === "radio") {
73
- stoOpGet(storageKey).then((v) => {
74
- const currentSelected =
75
- v !== undefined && v !== null ? v : storageValue.selected;
76
- if (option === currentSelected) {
77
- eleInput.checked = true;
98
+ // It is recommended to bind the 'change' event to the input rather than 'onclick' to the label
99
+ eleInput.addEventListener("change", () => {
100
+ stoOpSet(storageKey, option).then(() => {
101
+ if (typeof radioItemClickCallback === "function") {
102
+ radioItemClickCallback(storageKey, option);
78
103
  }
79
-
80
- // Initial visibility evaluation
81
- triggerVisibility(storageKey, currentSelected);
104
+ triggerVisibility(storageKey, option);
82
105
  });
106
+ });
107
+ }
83
108
 
84
- eleLabel.onclick = function () {
85
- stoOpSet(storageKey, option).then(() => {
86
- if (typeof radioItemClickCallback === "function") {
87
- radioItemClickCallback(storageKey, option);
88
- }
89
-
90
- // Dynamic visibility update
91
- triggerVisibility(storageKey, option);
92
- });
93
- };
94
- }
95
-
96
- eleLabel.append(eleInput);
97
- return eleLabel;
98
- })
99
- .forEach((ele) => eleWrap.append(ele));
109
+ // Fix: Native HTML typically places the input element before the text node inside a label.
110
+ eleLabel.prepend(eleInput);
111
+ eleWrap.append(eleLabel);
112
+ });
100
113
  }
101
114
 
102
115
  // --- CONDITION 2: TOGGLE BUTTON ---
103
116
  else if (type === "button") {
104
117
  const eleButton = document.createElement("button");
105
- eleButton.type = "button"; // Prevent accidental form submissions
106
-
107
- stoOpGet(storageKey).then((v) => {
108
- // Fallback to default schema configuration if no value is stored yet
109
- let currentStatus =
110
- v !== undefined && v !== null
111
- ? v === true || v === "true"
112
- : storageValue.selected;
113
- eleButton.textContent = String(currentStatus);
118
+ eleButton.type = "button";
114
119
 
115
- // Initial visibility evaluation
116
- triggerVisibility(storageKey, currentStatus);
120
+ let currentStatus = initialValue === true || initialValue === "true";
121
+ eleButton.textContent = String(currentStatus); // Apply state synchronously
117
122
 
118
- eleButton.addEventListener("click", async () => {
119
- currentStatus = !currentStatus; // Toggle state
120
- eleButton.textContent = String(currentStatus);
121
- await stoOpSet(storageKey, currentStatus);
122
-
123
- // Dynamic visibility update
124
- triggerVisibility(storageKey, currentStatus);
125
- });
123
+ eleButton.addEventListener("click", async () => {
124
+ currentStatus = !currentStatus;
125
+ eleButton.textContent = String(currentStatus);
126
+ await stoOpSet(storageKey, currentStatus);
127
+ triggerVisibility(storageKey, currentStatus);
126
128
  });
127
129
 
128
130
  eleWrap.append(eleButton);
@@ -133,25 +135,19 @@ export function generateHtmlByUserSettings(
133
135
  const eleInput = document.createElement("input");
134
136
  eleInput.type = type;
135
137
  eleInput.name = storageKey;
138
+ eleInput.value = initialValue !== undefined ? initialValue : ""; // Apply state synchronously
136
139
 
137
- stoOpGet(storageKey).then((v) => {
138
- const currentVal =
139
- v !== undefined && v !== null ? v : storageValue.selected;
140
- eleInput.value = currentVal;
141
-
142
- // Initial visibility evaluation
143
- triggerVisibility(storageKey, currentVal);
144
- });
145
-
146
- // Updates storage on every keystroke/change execution
147
- eleInput.addEventListener("input", async () => {
148
- const rawValue = eleInput.value;
149
- const finalizedValue = type === "number" ? Number(rawValue) : rawValue;
150
-
140
+ // [Optimization 4] Wrap the write operation with debounce, delaying storage writes by 500ms.
141
+ // This eliminates UI freezing regardless of typing speed.
142
+ const debouncedSave = debounce(async (val) => {
143
+ const finalizedValue = type === "number" ? Number(val) : val;
151
144
  await stoOpSet(storageKey, finalizedValue);
145
+ }, 500);
152
146
 
153
- // Dynamic visibility update
154
- triggerVisibility(storageKey, finalizedValue);
147
+ eleInput.addEventListener("input", () => {
148
+ const rawValue = eleInput.value;
149
+ debouncedSave(rawValue);
150
+ triggerVisibility(storageKey, rawValue); // Visual UI updates do not require a delay
155
151
  });
156
152
 
157
153
  eleWrap.append(eleInput);
@@ -160,25 +156,22 @@ export function generateHtmlByUserSettings(
160
156
  // --- CONDITION 4: SPAN / READ-ONLY TEXT ---
161
157
  else if (type === "span") {
162
158
  const eleSpan = document.createElement("span");
163
- // Optional: Add a class for styling read-only text differently
164
- // eleSpan.className = 'read-only-text';
165
-
166
- stoOpGet(storageKey).then((v) => {
167
- // Fallback to default schema configuration if no value is stored yet
168
- const currentVal =
169
- v !== undefined && v !== null ? v : storageValue.selected;
170
-
171
- // Render as plain text
172
- eleSpan.textContent = String(currentVal);
173
-
174
- // Initial visibility evaluation
175
- triggerVisibility(storageKey, currentVal);
176
- });
177
-
159
+ eleSpan.textContent = String(initialValue); // Apply state synchronously
178
160
  eleWrap.append(eleSpan);
179
161
  }
162
+ });
180
163
 
181
- return eleWrap;
164
+ // [Optimization 5] Third pass: Globally trigger a single visibility check.
165
+ // At this point, all DOM elements exist and all initialValues are fully derived,
166
+ // preventing undefined errors or invalid target hiding.
167
+ keys.forEach((storageKey) => {
168
+ const storedValue = storageData[storageKey];
169
+ const initialValue =
170
+ storedValue !== undefined && storedValue !== null
171
+ ? storedValue
172
+ : userSettings[storageKey].selected;
173
+
174
+ triggerVisibility(storageKey, initialValue);
182
175
  });
183
176
 
184
177
  /**
@@ -192,14 +185,13 @@ export function generateHtmlByUserSettings(
192
185
  const targetElement = elementsMap[targetField];
193
186
 
194
187
  if (targetElement) {
195
- // String conversion guarantees type safety (e.g., matching boolean true against string "true")
196
188
  const shouldBeVisible = String(currentValue) === String(expectedValue);
197
189
  targetElement.style.display = shouldBeVisible ? "" : "none";
198
190
  }
199
191
  }
200
192
  }
201
193
 
202
- return fieldsets;
194
+ return fragment;
203
195
  }
204
196
 
205
197
  export function generateMkvScriptForSystemWindows({ vid, title }) {