@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 +1 -1
- package/package.json +1 -1
- package/src/browserDownload.js +11 -8
- package/src/generate.js +116 -124
package/README.md
CHANGED
package/package.json
CHANGED
package/src/browserDownload.js
CHANGED
|
@@ -10,11 +10,14 @@ export async function browserDownloadByDownlink(
|
|
|
10
10
|
downlink,
|
|
11
11
|
filename = null,
|
|
12
12
|
}) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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}
|
|
7
|
-
* @param {Function}
|
|
8
|
-
* @returns {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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";
|
|
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
|
-
|
|
116
|
-
|
|
120
|
+
let currentStatus = initialValue === true || initialValue === "true";
|
|
121
|
+
eleButton.textContent = String(currentStatus); // Apply state synchronously
|
|
117
122
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
194
|
+
return fragment;
|
|
203
195
|
}
|
|
204
196
|
|
|
205
197
|
export function generateMkvScriptForSystemWindows({ vid, title }) {
|