@nsnanocat/preference-panes 1.2.5 → 1.3.0
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/CHANGELOG.md +18 -0
- package/README.md +61 -4
- package/dist/home.html +1032 -0
- package/dist/module/index.html +2 -3
- package/dist/module/index.mjs +111 -58
- package/dist/module/navigation.mjs +1 -1
- package/dist/preference-panes.mjs +111 -58
- package/package.json +3 -2
- package/src/browser/ModuleFrame.mjs +1 -1
- package/src/browser/boxjs.mjs +38 -8
- package/src/browser/client.d.mts +2 -2
- package/src/browser/client.mjs +51 -21
- package/src/browser/home.d.mts +54 -0
- package/src/browser/home.html +16 -0
- package/src/browser/home.mjs +441 -0
- package/src/browser/index.d.ts +2 -0
- package/src/browser/module.html +1 -2
- package/src/browser/panel.mjs +22 -29
- package/src/browser/{panel.css → styles.css} +245 -3
- package/src/index.d.ts +8 -0
|
@@ -27,6 +27,7 @@ function normalizeBoxJs(config, module) {
|
|
|
27
27
|
if (!app || !Array.isArray(app.settings)) throw new TypeError("Expected BoxJS settings array");
|
|
28
28
|
for (const entry of app.settings) {
|
|
29
29
|
if (typeof entry.id !== "string") throw new TypeError("BoxJS settings require string IDs");
|
|
30
|
+
entry.id = entry.id.replace(/\[(0|[1-9]\d*)\]$/, ".$1");
|
|
30
31
|
if (!entry.id.startsWith("@")) {
|
|
31
32
|
if (Array.isArray(document)) throw new TypeError("BoxJS settings require @root.path IDs");
|
|
32
33
|
continue;
|
|
@@ -68,7 +69,7 @@ function normalizeBoxJs(config, module) {
|
|
|
68
69
|
};
|
|
69
70
|
if (type === "select" && !["string", "number", "boolean"].includes(field.type)) throw new TypeError(`Select requires a scalar val: ${entry.id}`);
|
|
70
71
|
if (entry.items) field.options = entry.items.map(item => ({ key: item.key, label: item.label }));
|
|
71
|
-
if (Object.
|
|
72
|
+
if (Object.prototype.hasOwnProperty.call(entry, "val")) field.defaultValue = normalizeStoredValue(field, entry.val);
|
|
72
73
|
if (
|
|
73
74
|
typeof field.name !== "string" ||
|
|
74
75
|
(field.placeholder !== undefined && typeof field.placeholder !== "string") ||
|
|
@@ -78,18 +79,42 @@ function normalizeBoxJs(config, module) {
|
|
|
78
79
|
)
|
|
79
80
|
throw new TypeError(`Invalid or overlapping BoxJS field: ${entry.id}`);
|
|
80
81
|
if (field.options && (new Set(field.options.map(item => item.key)).size !== field.options.length || field.options.some(item => !scalar(item.key) || typeof item.label !== "string"))) throw new TypeError(`Invalid options: ${entry.id}`);
|
|
81
|
-
if (Object.
|
|
82
|
-
if (field.control === "url" && !Object.
|
|
82
|
+
if (Object.prototype.hasOwnProperty.call(field, "defaultValue") && !validValue(field, field.defaultValue)) throw new TypeError(`Invalid BoxJS val: ${entry.id}`);
|
|
83
|
+
if (field.control === "url" && !Object.prototype.hasOwnProperty.call(field, "defaultValue")) throw new TypeError(`URL BoxJS settings require a val: ${entry.id}`);
|
|
83
84
|
fields.push(field);
|
|
84
85
|
}
|
|
85
86
|
if (!fields.length) throw new TypeError(`No BoxJS settings for module: ${target.module}`);
|
|
86
|
-
const
|
|
87
|
+
for (const field of fields) {
|
|
88
|
+
const indexed = /^(.*)\.(\d+)$/.exec(field.key);
|
|
89
|
+
if (!indexed) continue;
|
|
90
|
+
const siblings = fields.filter(candidate => candidate.key.startsWith(`${indexed[1]}.`) && /^\d+$/.test(candidate.key.slice(indexed[1].length + 1)));
|
|
91
|
+
if (fields.some(candidate => candidate.key.startsWith(`${indexed[1]}.`) && !siblings.includes(candidate))) throw new TypeError(`Indexed settings cannot mix array entries and named children: ${indexed[1]}`);
|
|
92
|
+
if (siblings.some(candidate => !Object.prototype.hasOwnProperty.call(candidate, "defaultValue")) || !siblings.some(candidate => candidate.key === `${indexed[1]}.${siblings.length - 1}`) || siblings.some(candidate => Number(candidate.key.slice(indexed[1].length + 1)) >= siblings.length))
|
|
93
|
+
throw new TypeError(`Indexed settings require contiguous indices and defaults: ${indexed[1]}`);
|
|
94
|
+
}
|
|
95
|
+
const owner = target.owners.size === 1 ? [...target.owners][0] : undefined;
|
|
96
|
+
const cachePath = owner?.cachePath ?? `@${target.storageKey}.${target.module}.Caches`;
|
|
97
|
+
if (typeof cachePath !== "string" || !cachePath.startsWith(`@${target.storageKey}.`)) throw new TypeError("cachePath must use the module storage root");
|
|
98
|
+
const cacheParts = validatePathParts(cachePath.slice(target.storageKey.length + 2).split("."));
|
|
99
|
+
if (cacheParts.length !== 2 || cacheParts[1] !== "Caches") throw new TypeError("cachePath must identify a module Caches subtree");
|
|
100
|
+
if (owner?.resetPaths !== undefined) {
|
|
101
|
+
if (!Array.isArray(owner.resetPaths) || !owner.resetPaths.length) throw new TypeError("resetPaths must be a nonempty array");
|
|
102
|
+
for (const path of owner.resetPaths) {
|
|
103
|
+
if (typeof path !== "string" || !path.startsWith(`@${target.storageKey}.${target.module}.Settings.`)) throw new TypeError("resetPaths must stay in this module's Settings");
|
|
104
|
+
validatePathParts(path.slice(target.storageKey.length + 2).split("."));
|
|
105
|
+
}
|
|
106
|
+
if (fields.some(field => !owner.resetPaths.some(path => `@${target.storageKey}.${field.key}` === path || `@${target.storageKey}.${field.key}`.startsWith(`${path}.`)))) throw new TypeError("resetPaths must cover the page settings");
|
|
107
|
+
}
|
|
108
|
+
const firstIndexed = /^(.*)\.(\d+)$/.exec(fields[0].key);
|
|
109
|
+
const common = (firstIndexed ? firstIndexed[1] : fields[0].key).split(".").slice(0, -1);
|
|
87
110
|
for (const field of fields) while (!field.key.startsWith(`${common.join(".")}.`)) common.pop();
|
|
88
111
|
return {
|
|
89
112
|
module: target.module,
|
|
90
113
|
storageKey: target.storageKey,
|
|
91
114
|
fields,
|
|
92
115
|
settingsPath: common,
|
|
116
|
+
...(owner?.cachePath === undefined ? {} : { cachePath }),
|
|
117
|
+
...(owner?.resetPaths === undefined ? {} : { resetPaths: [...owner.resetPaths] }),
|
|
93
118
|
...(Object.keys(metadata).length ? { metadata } : {}),
|
|
94
119
|
};
|
|
95
120
|
}
|
|
@@ -179,9 +204,14 @@ function scalar(value) {
|
|
|
179
204
|
* @returns {boolean} 是否可由控件表示 / Whether the control can represent the value.
|
|
180
205
|
*/
|
|
181
206
|
function validValueShape(field, value) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
207
|
+
switch (field.type) {
|
|
208
|
+
case "array":
|
|
209
|
+
if (!Array.isArray(value) || value.some(item => !scalar(item)) || new Set(value).size !== value.length) return false;
|
|
210
|
+
break;
|
|
211
|
+
default:
|
|
212
|
+
if (typeof value !== field.type || !scalar(value)) return false;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
185
215
|
if (field.control === "url" && (!/^[a-z][a-z\d+.-]*:/i.test(value) || /^(?:javascript|data|vbscript):/i.test(value))) return false;
|
|
186
216
|
return true;
|
|
187
217
|
}
|
|
@@ -211,7 +241,7 @@ function resolveStoredValue(field, stored) {
|
|
|
211
241
|
if (value === undefined) return {};
|
|
212
242
|
if (!validValueShape(field, value)) {
|
|
213
243
|
return {
|
|
214
|
-
...(Object.
|
|
244
|
+
...(Object.prototype.hasOwnProperty.call(field, "defaultValue") ? { value: field.defaultValue } : {}),
|
|
215
245
|
warning: { kind: "invalid-value", values: [stored] },
|
|
216
246
|
};
|
|
217
247
|
}
|
|
@@ -497,6 +527,7 @@ class PreferencesClient {
|
|
|
497
527
|
#values = {};
|
|
498
528
|
#warnings = {};
|
|
499
529
|
#saving = false;
|
|
530
|
+
#arrays = new Map();
|
|
500
531
|
|
|
501
532
|
/**
|
|
502
533
|
* 创建从 BoxJS 定义读取和持久化设置的页面客户端。
|
|
@@ -523,13 +554,22 @@ class PreferencesClient {
|
|
|
523
554
|
if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
|
|
524
555
|
const values = {};
|
|
525
556
|
const warnings = {};
|
|
557
|
+
this.#arrays.clear();
|
|
526
558
|
for (const field of this.#definition.fields) {
|
|
559
|
+
const indexed = /^(.*)\.(\d+)$/.exec(field.key);
|
|
560
|
+
if (indexed && !this.#arrays.has(indexed[1])) {
|
|
561
|
+
const storedArray = indexed[1]
|
|
562
|
+
.split(".")
|
|
563
|
+
.slice(this.#definition.settingsPath.length)
|
|
564
|
+
.reduce((parent, part) => Object(parent)[part], subtree);
|
|
565
|
+
this.#arrays.set(indexed[1], Array.isArray(storedArray) ? [...storedArray] : []);
|
|
566
|
+
}
|
|
527
567
|
const stored = field.key
|
|
528
568
|
.split(".")
|
|
529
569
|
.slice(this.#definition.settingsPath.length)
|
|
530
570
|
.reduce((parent, part) => Object(parent)[part], subtree);
|
|
531
571
|
const resolved = resolveStoredValue(field, stored);
|
|
532
|
-
if (Object.
|
|
572
|
+
if (Object.prototype.hasOwnProperty.call(resolved, "value")) values[field.key] = resolved.value;
|
|
533
573
|
if (resolved.warning) warnings[field.key] = resolved.warning;
|
|
534
574
|
}
|
|
535
575
|
this.#values = values;
|
|
@@ -543,7 +583,7 @@ class PreferencesClient {
|
|
|
543
583
|
* @returns {import("./client.mjs").ModuleSnapshot} 会话快照 / Session snapshot.
|
|
544
584
|
*/
|
|
545
585
|
snapshot() {
|
|
546
|
-
return
|
|
586
|
+
return JSON.parse(JSON.stringify({ definition: this.#definition, values: this.#values, warnings: this.#warnings }));
|
|
547
587
|
}
|
|
548
588
|
|
|
549
589
|
/**
|
|
@@ -562,22 +602,23 @@ class PreferencesClient {
|
|
|
562
602
|
* @returns {Promise<unknown>} Caches 内容或 undefined / Caches content or undefined.
|
|
563
603
|
*/
|
|
564
604
|
async readCaches() {
|
|
565
|
-
const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#module}.Caches`);
|
|
605
|
+
const response = await this.#send("get", this.#definition.cachePath ?? `@${this.#definition.storageKey}.${this.#module}.Caches`);
|
|
566
606
|
return response.status === 404 ? undefined : response.json();
|
|
567
607
|
}
|
|
568
608
|
|
|
569
609
|
/**
|
|
570
|
-
*
|
|
571
|
-
* Delete the current module Caches
|
|
610
|
+
* 删除声明的缓存子树,默认当前模块 Caches。
|
|
611
|
+
* Delete the declared cache subtree, defaulting to the current module Caches.
|
|
572
612
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
573
613
|
*/
|
|
574
614
|
clearCaches() {
|
|
575
|
-
|
|
615
|
+
const path = this.#definition.cachePath?.slice(this.#definition.storageKey.length + 2) ?? `${this.#module}.Caches`;
|
|
616
|
+
return this.#change("delete", path, undefined, "clearCaches");
|
|
576
617
|
}
|
|
577
618
|
|
|
578
619
|
/**
|
|
579
|
-
*
|
|
580
|
-
* Delete current module data and restore page defaults.
|
|
620
|
+
* 删除声明的设置子树,默认删除当前模块数据,并恢复页面默认值。
|
|
621
|
+
* Delete declared settings subtrees, defaulting to current module data, and restore page defaults.
|
|
581
622
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
582
623
|
*/
|
|
583
624
|
reset() {
|
|
@@ -658,30 +699,49 @@ class PreferencesClient {
|
|
|
658
699
|
if (this.#saving) throw new Error("A settings write is already in progress");
|
|
659
700
|
this.#saving = true;
|
|
660
701
|
try {
|
|
661
|
-
let field;
|
|
662
|
-
if (operation === "write" || operation === "delete") {
|
|
663
|
-
field = this.#definition.fields.find(candidate => candidate.key === key);
|
|
664
|
-
if (!field || (operation === "write" && !validValue(field, value))) throw new TypeError("Invalid setting value");
|
|
665
|
-
}
|
|
666
|
-
await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
|
|
667
702
|
switch (operation) {
|
|
668
703
|
case "write":
|
|
669
|
-
this.#values[key] = structuredClone(value);
|
|
670
|
-
delete this.#warnings[key];
|
|
671
|
-
break;
|
|
672
704
|
case "delete": {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
705
|
+
const field = this.#definition.fields.find(candidate => candidate.key === key);
|
|
706
|
+
if (!field || (operation === "write" && !validValue(field, value))) throw new TypeError("Invalid setting value");
|
|
707
|
+
const indexed = /^(.*)\.(\d+)$/.exec(key);
|
|
708
|
+
if (indexed) {
|
|
709
|
+
const array = [...(this.#arrays.get(indexed[1]) ?? [])];
|
|
710
|
+
for (const sibling of this.#definition.fields) {
|
|
711
|
+
if (sibling.key.startsWith(`${indexed[1]}.`) && /^\d+$/.test(sibling.key.slice(indexed[1].length + 1))) {
|
|
712
|
+
const index = Number(sibling.key.slice(indexed[1].length + 1));
|
|
713
|
+
if (array[index] === undefined) array[index] = this.#values[sibling.key] ?? sibling.defaultValue;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
array[Number(indexed[2])] = operation === "delete" ? field.defaultValue : value;
|
|
717
|
+
await this.#send("set", `@${this.#definition.storageKey}.${indexed[1]}`, array);
|
|
718
|
+
this.#arrays.set(indexed[1], array);
|
|
719
|
+
} else await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
|
|
720
|
+
switch (operation) {
|
|
721
|
+
case "write":
|
|
722
|
+
this.#values[key] = JSON.parse(JSON.stringify(value));
|
|
723
|
+
delete this.#warnings[key];
|
|
724
|
+
break;
|
|
725
|
+
case "delete":
|
|
726
|
+
delete this.#values[key];
|
|
727
|
+
delete this.#warnings[key];
|
|
728
|
+
if (Object.prototype.hasOwnProperty.call(field, "defaultValue")) this.#values[key] = JSON.parse(JSON.stringify(field.defaultValue));
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
676
731
|
break;
|
|
677
732
|
}
|
|
678
733
|
case "clearCaches":
|
|
734
|
+
await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
|
|
679
735
|
break;
|
|
680
736
|
case "reset":
|
|
737
|
+
if (this.#definition.resetPaths) {
|
|
738
|
+
for (const path of this.#definition.resetPaths) await this.#send("delete", path);
|
|
739
|
+
} else await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
|
|
740
|
+
this.#arrays.clear();
|
|
681
741
|
this.#warnings = {};
|
|
682
742
|
for (const field of this.#definition.fields) {
|
|
683
743
|
delete this.#values[field.key];
|
|
684
|
-
if (Object.
|
|
744
|
+
if (Object.prototype.hasOwnProperty.call(field, "defaultValue")) this.#values[field.key] = JSON.parse(JSON.stringify(field.defaultValue));
|
|
685
745
|
}
|
|
686
746
|
break;
|
|
687
747
|
}
|
|
@@ -962,17 +1022,17 @@ class PreferencesPanel {
|
|
|
962
1022
|
const notify = event => {
|
|
963
1023
|
if (destroyed) return;
|
|
964
1024
|
let message;
|
|
965
|
-
switch (
|
|
966
|
-
case
|
|
1025
|
+
switch (event.kind === "error" ? "error" : event.operation) {
|
|
1026
|
+
case "error":
|
|
967
1027
|
message = `操作失败:${event.message}`;
|
|
968
1028
|
break;
|
|
969
|
-
case
|
|
1029
|
+
case "delete":
|
|
970
1030
|
message = "删除成功";
|
|
971
1031
|
break;
|
|
972
|
-
case
|
|
1032
|
+
case "clearCaches":
|
|
973
1033
|
message = "Caches 已清空";
|
|
974
1034
|
break;
|
|
975
|
-
case
|
|
1035
|
+
case "reset":
|
|
976
1036
|
message = "设置已重置";
|
|
977
1037
|
break;
|
|
978
1038
|
default:
|
|
@@ -1095,21 +1155,6 @@ class PreferencesPanel {
|
|
|
1095
1155
|
});
|
|
1096
1156
|
return queue;
|
|
1097
1157
|
}
|
|
1098
|
-
const metadata = definition.metadata;
|
|
1099
|
-
if (metadata) {
|
|
1100
|
-
const info = element("div", "pp-module-info");
|
|
1101
|
-
const details = element("div", "pp-module-details");
|
|
1102
|
-
for (const description of [metadata.author, metadata.desc ?? metadata.description, ...(metadata.descs ?? [])]) if (description) details.append(element("p", "pp-description", description));
|
|
1103
|
-
if (metadata.repo) {
|
|
1104
|
-
const link = element("a", "pp-module-source", "项目主页");
|
|
1105
|
-
link.href = resourceURL(metadata.repo);
|
|
1106
|
-
link.target = "_blank";
|
|
1107
|
-
link.rel = "noopener noreferrer";
|
|
1108
|
-
details.append(link);
|
|
1109
|
-
}
|
|
1110
|
-
info.append(details);
|
|
1111
|
-
view.append(info);
|
|
1112
|
-
}
|
|
1113
1158
|
for (const field of definition.fields) {
|
|
1114
1159
|
const match = /^\[([^\]]+)\]\s*(.*)$/.exec(field.name);
|
|
1115
1160
|
const group = match?.[1] ?? "通用";
|
|
@@ -1150,8 +1195,8 @@ class PreferencesPanel {
|
|
|
1150
1195
|
let write;
|
|
1151
1196
|
let inputContainer = row;
|
|
1152
1197
|
let eventName = "change";
|
|
1153
|
-
switch (
|
|
1154
|
-
case
|
|
1198
|
+
switch (field.control === "url" ? "url" : field.options ? (field.type === "array" ? "checkboxes" : "selects") : field.type) {
|
|
1199
|
+
case "url": {
|
|
1155
1200
|
const link = element("a", "pp-choice-link", "打开");
|
|
1156
1201
|
link.setAttribute("aria-label", field.name);
|
|
1157
1202
|
link.rel = "noopener noreferrer";
|
|
@@ -1167,7 +1212,7 @@ class PreferencesPanel {
|
|
|
1167
1212
|
});
|
|
1168
1213
|
break;
|
|
1169
1214
|
}
|
|
1170
|
-
case
|
|
1215
|
+
case "selects": {
|
|
1171
1216
|
const select = element("select", "");
|
|
1172
1217
|
select.setAttribute("aria-label", field.name);
|
|
1173
1218
|
field.options.forEach((option, index) => {
|
|
@@ -1182,7 +1227,7 @@ class PreferencesPanel {
|
|
|
1182
1227
|
read = () => field.options[select.selectedIndex]?.key;
|
|
1183
1228
|
break;
|
|
1184
1229
|
}
|
|
1185
|
-
case
|
|
1230
|
+
case "checkboxes": {
|
|
1186
1231
|
const page = element("section", "pp-choice-page");
|
|
1187
1232
|
if (field.description) page.append(element("p", "pp-description", field.description));
|
|
1188
1233
|
const choices = element("div", "pp-rows");
|
|
@@ -1226,7 +1271,7 @@ class PreferencesPanel {
|
|
|
1226
1271
|
};
|
|
1227
1272
|
break;
|
|
1228
1273
|
}
|
|
1229
|
-
case
|
|
1274
|
+
case "boolean": {
|
|
1230
1275
|
const toggle = element("input", "pp-switch");
|
|
1231
1276
|
toggle.type = "checkbox";
|
|
1232
1277
|
toggle.setAttribute("switch", "");
|
|
@@ -1264,7 +1309,12 @@ class PreferencesPanel {
|
|
|
1264
1309
|
growingInputs.push(grow);
|
|
1265
1310
|
}
|
|
1266
1311
|
eventName = "input";
|
|
1267
|
-
if (!multiline)
|
|
1312
|
+
if (!multiline) {
|
|
1313
|
+
input.type = field.type === "number" ? "number" : "text";
|
|
1314
|
+
input.autocapitalize = "off";
|
|
1315
|
+
input.setAttribute("autocorrect", "off");
|
|
1316
|
+
input.spellcheck = false;
|
|
1317
|
+
}
|
|
1268
1318
|
write = value => {
|
|
1269
1319
|
input.value = field.type === "array" ? JSON.stringify(value ?? []) : (value ?? "");
|
|
1270
1320
|
grow();
|
|
@@ -1366,7 +1416,9 @@ class PreferencesPanel {
|
|
|
1366
1416
|
});
|
|
1367
1417
|
handlers.set("clearCaches", async () => {
|
|
1368
1418
|
if (saving) return;
|
|
1369
|
-
|
|
1419
|
+
const shared = definition.cachePath && definition.cachePath !== `@${definition.storageKey}.${active}.Caches`;
|
|
1420
|
+
const message = shared ? `清空共享缓存 ${definition.cachePath}?使用该缓存的其它模块也会受影响。` : `清空 ${active} 的全部 Caches?`;
|
|
1421
|
+
if (!(await requestConfirmation(window, message)) || destroyed || saving) return;
|
|
1370
1422
|
return perform(
|
|
1371
1423
|
() => client.clearCaches(),
|
|
1372
1424
|
() => {
|
|
@@ -1376,7 +1428,8 @@ class PreferencesPanel {
|
|
|
1376
1428
|
});
|
|
1377
1429
|
handlers.set("reset", async () => {
|
|
1378
1430
|
if (saving) return;
|
|
1379
|
-
|
|
1431
|
+
const message = definition.resetPaths ? `重置 ${definition.metadata?.name ?? active} 的设置?只删除本页面的设置,保留共享存储中的其它设置和缓存。` : `重置 ${active} 的设置?这将删除该模块的 Settings、Caches 和其它持久化数据。`;
|
|
1432
|
+
if (!(await requestConfirmation(window, message)) || destroyed || saving) return;
|
|
1380
1433
|
return perform(() => client.reset(), controls);
|
|
1381
1434
|
});
|
|
1382
1435
|
navigation?.destroy();
|
|
@@ -1419,7 +1472,7 @@ class PreferencesPanel {
|
|
|
1419
1472
|
}
|
|
1420
1473
|
}
|
|
1421
1474
|
|
|
1422
|
-
var defaults = "/* 通用默认样式只使用 pp 命名空间;项目可通过 CSS 输入覆盖变量和组件。\n * Generic defaults use only the pp namespace; projects may override variables and components through CSS input. */\n.pp-panel {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-field: #f1f2f3;\n --pp-border: #e3e5e7;\n --pp-muted: #797f87;\n --pp-accent: #1677ff;\n --pp-warning: #9a6700;\n font:\n 15px / 1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif;\n color: var(--pp-text);\n background: var(--pp-background);\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n max-width: 100vw;\n min-width: 0;\n height: 100vh;\n overflow: hidden;\n}\n\n:root[data-theme=\"dark\"] .pp-panel {\n --pp-text: #f1f2f3;\n --pp-background: #0d0e0f;\n --pp-surface: #18191c;\n --pp-field: #2f3238;\n --pp-border: #2f3238;\n --pp-muted: #9499a0;\n --pp-warning: #f0b849;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\n flex: none;\n height: calc(52px + env(safe-area-inset-top));\n padding: env(safe-area-inset-top) 12px 0;\n display: flex;\n align-items: center;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n position: relative;\n z-index: 2;\n}\n.pp-title {\n flex: 1;\n text-align: center;\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-nav-spacer {\n width: 44px;\n flex: none;\n}\n.pp-panel button {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: none;\n color: inherit;\n}\n.pp-panel .pp-back {\n width: 44px;\n height: 44px;\n flex: none;\n font-size: 34px;\n line-height: 32px;\n padding: 0;\n}\n.pp-panel button:disabled {\n opacity: 0.5;\n cursor: wait;\n}\n.pp-viewport {\n position: relative;\n flex: 1;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n@supports (height: 100dvh) {\n .pp-panel {\n height: 100dvh;\n }\n}\n.pp-fields,\n.pp-choice-page,\n.pp-settings-page,\n.pp-cache-page {\n position: absolute;\n inset: 0;\n min-width: 0;\n overflow-x: hidden;\n overflow-y: auto;\n padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom) + var(--pp-keyboard-height, 0px));\n scroll-padding-bottom: var(--pp-keyboard-height, 0px);\n background: var(--pp-background);\n}\n.pp-choice-link {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n max-width: 45%;\n min-width: 44px;\n min-height: 44px;\n padding: 0;\n text-align: right;\n flex: 1;\n}\n.pp-panel a.pp-choice-link {\n color: var(--pp-accent);\n text-decoration: none;\n}\n.pp-summary {\n color: var(--pp-muted);\n font-size: 13px;\n line-height: 18px;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.pp-chevron {\n color: var(--pp-muted);\n font-size: 22px;\n flex: none;\n}\n.pp-editor {\n flex: none;\n width: 45%;\n min-width: 0;\n min-height: 36px;\n padding: 8px 10px;\n font: inherit;\n color: var(--pp-text);\n background: var(--pp-field);\n border: 0;\n border-radius: 6px;\n}\n.pp-panel .pp-multiline {\n display: block;\n}\n.pp-multiline .pp-editor {\n width: 100%;\n margin-top: 10px;\n}\n.pp-panel [hidden] {\n display: none !important;\n}\n.pp-label {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n margin-right: 16px;\n}\n.pp-field-name {\n color: var(--pp-text);\n font-size: 15px;\n}\n.pp-field-description {\n margin-top: 2px;\n color: var(--pp-muted);\n font-size: 12px;\n}\n.pp-field-warning {\n margin-top: 4px;\n color: var(--pp-warning);\n font-size: 12px;\n overflow-wrap: anywhere;\n}\n.pp-group {\n margin-top: 16px;\n}\n.pp-group-title {\n margin: 0 0 8px;\n color: var(--pp-muted);\n font-size: 15px;\n font-weight: 400;\n}\n.pp-row {\n min-width: 0;\n min-height: 48px;\n padding: 16px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n}\n.pp-rows > :last-child {\n border-bottom: 0 !important;\n}\n.pp-switch {\n flex: none;\n accent-color: var(--pp-accent);\n}\n.pp-choice {\n justify-content: space-between;\n cursor: pointer;\n}\n.pp-choice input {\n width: 20px;\n height: 20px;\n flex: none;\n accent-color: var(--pp-accent);\n margin: 0;\n}\n.pp-description {\n font-size: 12px;\n line-height: 1.6;\n color: var(--pp-muted);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-module-info {\n display: flex;\n gap: 12px;\n margin: 12px 0;\n}\n.pp-module-details {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-module-source {\n color: inherit;\n text-decoration: underline;\n}\n.pp-status {\n position: fixed;\n inset: 0;\n display: grid;\n place-content: center;\n justify-items: center;\n gap: 12px;\n min-width: 0;\n min-height: 0;\n margin: 0;\n padding: 24px;\n color: var(--pp-muted, GrayText);\n text-align: center;\n background: var(--pp-background, Canvas);\n}\n.pp-viewport > .pp-status {\n position: absolute;\n}\n.pp-status-spinner {\n box-sizing: border-box;\n width: 28px;\n height: 28px;\n border: 3px solid color-mix(in srgb, currentColor 25%, transparent);\n border-top-color: var(--pp-accent, AccentColor);\n border-radius: 50%;\n animation: pp-status-spin 0.8s linear infinite;\n}\n.pp-status-message {\n max-width: 100%;\n margin: 0;\n overflow-wrap: anywhere;\n}\n.pp-status-action {\n min-width: 96px;\n min-height: 44px;\n padding: 8px 16px;\n border: 0;\n border-radius: 6px;\n color: var(--pp-text, ButtonText);\n font: inherit;\n cursor: pointer;\n background: var(--pp-surface, ButtonFace);\n}\n@keyframes pp-status-spin {\n to {\n transform: rotate(1turn);\n }\n}\n.pp-cache {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-toast {\n pointer-events: none;\n position: fixed;\n bottom: calc(30px + env(safe-area-inset-bottom));\n left: 50%;\n transform: translateX(-50%);\n max-width: 90vw;\n padding: 10px 16px;\n border-radius: 8px;\n background: #333e;\n color: white;\n font-size: 13px;\n z-index: 20;\n}\n.pp-toast[data-kind=\"error\"] {\n background: #8d2424;\n}\n.pp-panel :focus-visible {\n outline: 2px solid var(--pp-accent);\n outline-offset: -2px;\n}\n";
|
|
1475
|
+
var defaults = "/* 通用默认样式只使用 pp 命名空间;项目可通过 CSS 输入覆盖变量和组件。\n * Generic defaults use only the pp namespace; projects may override variables and components through CSS input. */\n:root {\n --pp-text: #18191c;\n --pp-background: #f6f7f8;\n --pp-surface: #fff;\n --pp-field: #f1f2f3;\n --pp-border: #e3e5e7;\n --pp-muted: #797f87;\n --pp-accent: #1677ff;\n --pp-warning: #9a6700;\n --pp-host-height: 0px;\n --pp-keyboard-height: 0px;\n}\n.pp-panel {\n font:\n 15px / 1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif;\n color: var(--pp-text);\n background: var(--pp-background);\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n max-width: 100vw;\n min-width: 0;\n height: 100vh;\n overflow: hidden;\n}\n\n:root[data-theme=\"dark\"] {\n --pp-text: #f1f2f3;\n --pp-background: #0d0e0f;\n --pp-surface: #18191c;\n --pp-field: #2f3238;\n --pp-border: #2f3238;\n --pp-muted: #9499a0;\n --pp-warning: #f0b849;\n}\n.pp-panel * {\n box-sizing: border-box;\n letter-spacing: 0;\n}\n.pp-header {\n flex: none;\n height: calc(52px + env(safe-area-inset-top));\n padding: env(safe-area-inset-top) 12px 0;\n display: flex;\n align-items: center;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n position: relative;\n z-index: 2;\n}\n.pp-title {\n flex: 1;\n text-align: center;\n font-size: 17px;\n font-weight: 500;\n margin: 0;\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-nav-spacer {\n width: 44px;\n flex: none;\n}\n.pp-panel button {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: none;\n color: inherit;\n}\n.pp-panel .pp-back {\n width: 44px;\n height: 44px;\n flex: none;\n font-size: 34px;\n line-height: 32px;\n padding: 0;\n}\n.pp-panel button:disabled {\n opacity: 0.5;\n cursor: wait;\n}\n.pp-viewport {\n position: relative;\n flex: 1;\n min-width: 0;\n min-height: 0;\n overflow: hidden;\n}\n@supports (height: 100dvh) {\n .pp-panel {\n height: 100dvh;\n }\n}\n.pp-fields,\n.pp-choice-page,\n.pp-settings-page,\n.pp-cache-page {\n position: absolute;\n inset: 0;\n min-width: 0;\n overflow-x: hidden;\n overflow-y: auto;\n padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom) + var(--pp-keyboard-height, 0px));\n scroll-padding-bottom: var(--pp-keyboard-height, 0px);\n background: var(--pp-background);\n}\n.pp-choice-link {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n max-width: 45%;\n min-width: 44px;\n min-height: 44px;\n padding: 0;\n text-align: right;\n flex: 1;\n}\n.pp-panel a.pp-choice-link {\n color: var(--pp-accent);\n text-decoration: none;\n}\n.pp-summary {\n color: var(--pp-muted);\n font-size: 13px;\n line-height: 18px;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n overflow-wrap: anywhere;\n}\n.pp-chevron {\n color: var(--pp-muted);\n font-size: 22px;\n flex: none;\n}\n.pp-editor {\n flex: none;\n width: 45%;\n min-width: 0;\n min-height: 36px;\n padding: 8px 10px;\n font: inherit;\n font-size: 16px;\n color: var(--pp-text);\n background: var(--pp-field);\n border: 0;\n border-radius: 6px;\n}\n.pp-panel .pp-multiline {\n display: block;\n}\n.pp-multiline .pp-editor {\n width: 100%;\n margin-top: 10px;\n}\n.pp-panel [hidden] {\n display: none !important;\n}\n.pp-label {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n margin-right: 16px;\n}\n.pp-field-name {\n color: var(--pp-text);\n font-size: 15px;\n}\n.pp-field-description {\n margin-top: 2px;\n color: var(--pp-muted);\n font-size: 12px;\n}\n.pp-field-warning {\n margin-top: 4px;\n color: var(--pp-warning);\n font-size: 12px;\n overflow-wrap: anywhere;\n}\n.pp-group {\n margin-top: 16px;\n}\n.pp-group-title {\n margin: 0 0 8px;\n color: var(--pp-muted);\n font-size: 15px;\n font-weight: 400;\n}\n.pp-row {\n min-width: 0;\n min-height: 48px;\n padding: 16px;\n display: flex;\n align-items: center;\n justify-content: space-between;\n background: var(--pp-surface);\n border-bottom: 1px solid var(--pp-border);\n}\n.pp-rows > :last-child {\n border-bottom: 0 !important;\n}\n.pp-switch {\n flex: none;\n accent-color: var(--pp-accent);\n}\n.pp-choice {\n justify-content: space-between;\n cursor: pointer;\n}\n.pp-choice input {\n width: 20px;\n height: 20px;\n flex: none;\n accent-color: var(--pp-accent);\n margin: 0;\n}\n.pp-description {\n font-size: 12px;\n line-height: 1.6;\n color: var(--pp-muted);\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-module-info {\n display: flex;\n gap: 12px;\n margin: 12px 0;\n}\n.pp-module-details {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n.pp-module-source {\n color: inherit;\n text-decoration: underline;\n}\n.pp-status {\n position: fixed;\n inset: 0;\n display: grid;\n place-content: center;\n justify-items: center;\n gap: 12px;\n min-width: 0;\n min-height: 0;\n margin: 0;\n padding: 24px;\n color: var(--pp-muted, GrayText);\n text-align: center;\n background: var(--pp-background, Canvas);\n}\n.pp-viewport > .pp-status {\n position: absolute;\n}\n.pp-status-spinner {\n box-sizing: border-box;\n width: 28px;\n height: 28px;\n border: 3px solid var(--pp-border, #8884);\n border-top-color: var(--pp-accent, AccentColor);\n border-radius: 50%;\n animation: pp-status-spin 0.8s linear infinite;\n}\n.pp-status-message {\n max-width: 100%;\n margin: 0;\n overflow-wrap: anywhere;\n}\n.pp-status-action {\n min-width: 96px;\n min-height: 44px;\n padding: 8px 16px;\n border: 0;\n border-radius: 6px;\n color: var(--pp-text, ButtonText);\n font: inherit;\n cursor: pointer;\n background: var(--pp-surface, ButtonFace);\n}\n@keyframes pp-status-spin {\n to {\n transform: rotate(1turn);\n }\n}\n.pp-cache {\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.pp-toast {\n pointer-events: none;\n position: fixed;\n bottom: calc(30px + env(safe-area-inset-bottom));\n left: 50%;\n transform: translateX(-50%);\n max-width: 90vw;\n padding: 10px 16px;\n border-radius: 8px;\n background: #333e;\n color: white;\n font-size: 13px;\n z-index: 20;\n}\n.pp-toast[data-kind=\"error\"] {\n background: #8d2424;\n}\n.pp-panel :focus-visible {\n outline: 2px solid var(--pp-accent);\n outline-offset: -2px;\n}\n\n/* 首页沿用表单的分组与说明样式,入口按屏幕宽度自动换行。\n * The landing page reuses form groups and descriptions, with entries wrapping by viewport width. */\n:root.pp-page {\n color-scheme: light dark;\n}\n.pp-page body {\n margin: 0;\n font:\n 16px / 1.5 -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n sans-serif;\n background: var(--pp-background, Canvas);\n color: var(--pp-text, CanvasText);\n}\n[data-preference-panes-pages] {\n position: relative;\n min-height: calc(100vh - var(--pp-host-height));\n background: inherit;\n}\n[data-preference-panes-module-page],\n[data-preference-panes-link-page] {\n position: fixed;\n inset: var(--pp-host-height) 0 0;\n background: inherit;\n z-index: 1;\n box-shadow: -8px 0 24px #00000014;\n}\n[data-preference-panes-module-page] iframe,\n[data-preference-panes-link-page] iframe {\n display: block;\n border: 0;\n width: 100%;\n height: 100%;\n background: inherit;\n}\n[data-module-message] {\n position: absolute;\n inset: 0;\n margin: 0;\n padding: 20px;\n background: inherit;\n}\n[data-module-message][hidden] {\n display: none;\n}\n.pp-host-header {\n position: sticky;\n top: 0;\n z-index: 2;\n display: grid;\n grid-template-columns: 44px minmax(0, 1fr) 44px;\n align-items: center;\n min-height: 44px;\n padding: env(safe-area-inset-top) max(12px, env(safe-area-inset-right)) 0 max(12px, env(safe-area-inset-left));\n border-bottom: 1px solid var(--pp-border, #8884);\n background: var(--pp-surface, Canvas);\n color: var(--pp-accent, Highlight);\n font-size: 17px;\n}\n.pp-host-title {\n text-align: center;\n color: var(--pp-text, CanvasText);\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.pp-host-back {\n border: 0;\n background: none;\n color: inherit;\n width: 44px;\n height: 44px;\n padding: 8px;\n font:\n 32px / 1 -apple-system,\n sans-serif;\n cursor: pointer;\n}\n.pp-host-back[hidden] {\n visibility: hidden;\n display: block;\n}\n.pp-host-toast {\n position: fixed;\n z-index: 3;\n bottom: calc(24px + env(safe-area-inset-bottom) + var(--pp-keyboard-height));\n left: 50%;\n transform: translateX(-50%);\n max-width: calc(100% - 48px);\n padding: 10px 18px;\n border-radius: 12px;\n background: #222e;\n color: #fff;\n font-size: 14px;\n text-align: center;\n}\n.pp-host-toast[hidden] {\n display: none;\n}\n.pp-host-error {\n margin: 24px;\n padding: 16px;\n border: 1px solid var(--pp-danger, #e45656);\n border-radius: 12px;\n font-size: 16px;\n overflow-wrap: anywhere;\n}\n\n.pp-home {\n max-width: 1000px;\n margin: auto;\n padding: 24px max(16px, env(safe-area-inset-right)) calc(24px + env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));\n}\n.pp-home-logo {\n display: block;\n width: 64px;\n height: 64px;\n margin: 0 auto 24px;\n object-fit: contain;\n}\n.pp-home-entries {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-around;\n row-gap: 16px;\n padding: 16px 0;\n background: var(--pp-surface);\n}\n.pp-home-item {\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n align-items: center;\n width: 25%;\n min-width: 0;\n padding: 0 4px;\n border: 0;\n background: none;\n color: var(--pp-text);\n font:\n 12px / 1.5 -apple-system,\n sans-serif;\n cursor: pointer;\n text-decoration: none;\n}\n@media (min-width: 500px) {\n .pp-home-item {\n width: calc(100% / 6);\n }\n}\n@media (min-width: 700px) {\n .pp-home-item {\n width: 12.5%;\n }\n}\n.pp-home-icon {\n box-sizing: border-box;\n width: 56px;\n height: 56px;\n padding: 8px;\n border-radius: 50%;\n background: var(--pp-field);\n font-size: 32px;\n line-height: 40px;\n}\n.pp-home-icon img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n.pp-home-name {\n margin-top: 8px;\n}\n.pp-home-content {\n min-width: 0;\n width: 100%;\n}\n.pp-home-content > span {\n display: block;\n}\n.pp-home-name,\n.pp-home-item [data-module-status] {\n width: 100%;\n overflow: hidden;\n text-align: center;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.pp-home-item [data-module-status] {\n min-height: 20px;\n}\n.pp-home-item:disabled {\n opacity: 0.45;\n cursor: default;\n}\n.pp-home-entries[data-layout=\"list\"] {\n display: block;\n padding: 0;\n}\n.pp-home-entries[data-layout=\"list\"] .pp-home-item {\n flex-direction: row;\n gap: 12px;\n width: 100%;\n padding: 12px 16px;\n font-size: 15px;\n}\n.pp-home-entries[data-layout=\"list\"] .pp-home-item + .pp-home-item {\n border-top: 1px solid var(--pp-border);\n}\n.pp-home-entries[data-layout=\"list\"] .pp-home-icon {\n flex: none;\n width: 40px;\n height: 40px;\n padding: 4px;\n font-size: 24px;\n line-height: 32px;\n}\n.pp-home-entries[data-layout=\"list\"] .pp-home-name {\n margin-top: 0;\n text-align: left;\n}\n.pp-home-arrow {\n color: var(--pp-muted);\n font-size: 24px;\n}\n.pp-home-footer {\n margin-top: 16px;\n text-align: center;\n white-space: normal;\n}\n.pp-home-footer p {\n margin: 8px 0 0;\n}\n";
|
|
1423
1476
|
|
|
1424
1477
|
const selector = "style[data-preference-panes-defaults]";
|
|
1425
1478
|
const stylesheetSelector = "link[data-preference-panes-stylesheet]";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nsnanocat/preference-panes",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Generic BoxJS settings frontend and persistence API for JavaScript proxy modules",
|
|
5
5
|
"author": "VirgilClyne <Virgil@nanocat.me>",
|
|
6
6
|
"homepage": "https://NSNanoCat.github.io/preference-panes",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
},
|
|
53
53
|
"files": [
|
|
54
54
|
"src",
|
|
55
|
-
"dist"
|
|
55
|
+
"dist",
|
|
56
|
+
"CHANGELOG.md"
|
|
56
57
|
],
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@biomejs/biome": "2.1.4",
|
|
@@ -79,7 +79,7 @@ export class ModuleFrame extends EventTarget {
|
|
|
79
79
|
const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit", signal: this.#controller.signal });
|
|
80
80
|
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
|
|
81
81
|
const html = await response.text();
|
|
82
|
-
this.#controller.signal.
|
|
82
|
+
if (this.#controller.signal.aborted) throw new DOMException("The operation was aborted", "AbortError");
|
|
83
83
|
this.element.srcdoc = html;
|
|
84
84
|
} finally {
|
|
85
85
|
clearTimeout(timer);
|
package/src/browser/boxjs.mjs
CHANGED
|
@@ -17,6 +17,7 @@ export function normalizeBoxJs(config, module) {
|
|
|
17
17
|
if (!app || !Array.isArray(app.settings)) throw new TypeError("Expected BoxJS settings array");
|
|
18
18
|
for (const entry of app.settings) {
|
|
19
19
|
if (typeof entry.id !== "string") throw new TypeError("BoxJS settings require string IDs");
|
|
20
|
+
entry.id = entry.id.replace(/\[(0|[1-9]\d*)\]$/, ".$1");
|
|
20
21
|
if (!entry.id.startsWith("@")) {
|
|
21
22
|
if (Array.isArray(document)) throw new TypeError("BoxJS settings require @root.path IDs");
|
|
22
23
|
continue;
|
|
@@ -58,7 +59,7 @@ export function normalizeBoxJs(config, module) {
|
|
|
58
59
|
};
|
|
59
60
|
if (type === "select" && !["string", "number", "boolean"].includes(field.type)) throw new TypeError(`Select requires a scalar val: ${entry.id}`);
|
|
60
61
|
if (entry.items) field.options = entry.items.map(item => ({ key: item.key, label: item.label }));
|
|
61
|
-
if (Object.
|
|
62
|
+
if (Object.prototype.hasOwnProperty.call(entry, "val")) field.defaultValue = normalizeStoredValue(field, entry.val);
|
|
62
63
|
if (
|
|
63
64
|
typeof field.name !== "string" ||
|
|
64
65
|
(field.placeholder !== undefined && typeof field.placeholder !== "string") ||
|
|
@@ -68,18 +69,42 @@ export function normalizeBoxJs(config, module) {
|
|
|
68
69
|
)
|
|
69
70
|
throw new TypeError(`Invalid or overlapping BoxJS field: ${entry.id}`);
|
|
70
71
|
if (field.options && (new Set(field.options.map(item => item.key)).size !== field.options.length || field.options.some(item => !scalar(item.key) || typeof item.label !== "string"))) throw new TypeError(`Invalid options: ${entry.id}`);
|
|
71
|
-
if (Object.
|
|
72
|
-
if (field.control === "url" && !Object.
|
|
72
|
+
if (Object.prototype.hasOwnProperty.call(field, "defaultValue") && !validValue(field, field.defaultValue)) throw new TypeError(`Invalid BoxJS val: ${entry.id}`);
|
|
73
|
+
if (field.control === "url" && !Object.prototype.hasOwnProperty.call(field, "defaultValue")) throw new TypeError(`URL BoxJS settings require a val: ${entry.id}`);
|
|
73
74
|
fields.push(field);
|
|
74
75
|
}
|
|
75
76
|
if (!fields.length) throw new TypeError(`No BoxJS settings for module: ${target.module}`);
|
|
76
|
-
const
|
|
77
|
+
for (const field of fields) {
|
|
78
|
+
const indexed = /^(.*)\.(\d+)$/.exec(field.key);
|
|
79
|
+
if (!indexed) continue;
|
|
80
|
+
const siblings = fields.filter(candidate => candidate.key.startsWith(`${indexed[1]}.`) && /^\d+$/.test(candidate.key.slice(indexed[1].length + 1)));
|
|
81
|
+
if (fields.some(candidate => candidate.key.startsWith(`${indexed[1]}.`) && !siblings.includes(candidate))) throw new TypeError(`Indexed settings cannot mix array entries and named children: ${indexed[1]}`);
|
|
82
|
+
if (siblings.some(candidate => !Object.prototype.hasOwnProperty.call(candidate, "defaultValue")) || !siblings.some(candidate => candidate.key === `${indexed[1]}.${siblings.length - 1}`) || siblings.some(candidate => Number(candidate.key.slice(indexed[1].length + 1)) >= siblings.length))
|
|
83
|
+
throw new TypeError(`Indexed settings require contiguous indices and defaults: ${indexed[1]}`);
|
|
84
|
+
}
|
|
85
|
+
const owner = target.owners.size === 1 ? [...target.owners][0] : undefined;
|
|
86
|
+
const cachePath = owner?.cachePath ?? `@${target.storageKey}.${target.module}.Caches`;
|
|
87
|
+
if (typeof cachePath !== "string" || !cachePath.startsWith(`@${target.storageKey}.`)) throw new TypeError("cachePath must use the module storage root");
|
|
88
|
+
const cacheParts = validatePathParts(cachePath.slice(target.storageKey.length + 2).split("."));
|
|
89
|
+
if (cacheParts.length !== 2 || cacheParts[1] !== "Caches") throw new TypeError("cachePath must identify a module Caches subtree");
|
|
90
|
+
if (owner?.resetPaths !== undefined) {
|
|
91
|
+
if (!Array.isArray(owner.resetPaths) || !owner.resetPaths.length) throw new TypeError("resetPaths must be a nonempty array");
|
|
92
|
+
for (const path of owner.resetPaths) {
|
|
93
|
+
if (typeof path !== "string" || !path.startsWith(`@${target.storageKey}.${target.module}.Settings.`)) throw new TypeError("resetPaths must stay in this module's Settings");
|
|
94
|
+
validatePathParts(path.slice(target.storageKey.length + 2).split("."));
|
|
95
|
+
}
|
|
96
|
+
if (fields.some(field => !owner.resetPaths.some(path => `@${target.storageKey}.${field.key}` === path || `@${target.storageKey}.${field.key}`.startsWith(`${path}.`)))) throw new TypeError("resetPaths must cover the page settings");
|
|
97
|
+
}
|
|
98
|
+
const firstIndexed = /^(.*)\.(\d+)$/.exec(fields[0].key);
|
|
99
|
+
const common = (firstIndexed ? firstIndexed[1] : fields[0].key).split(".").slice(0, -1);
|
|
77
100
|
for (const field of fields) while (!field.key.startsWith(`${common.join(".")}.`)) common.pop();
|
|
78
101
|
return {
|
|
79
102
|
module: target.module,
|
|
80
103
|
storageKey: target.storageKey,
|
|
81
104
|
fields,
|
|
82
105
|
settingsPath: common,
|
|
106
|
+
...(owner?.cachePath === undefined ? {} : { cachePath }),
|
|
107
|
+
...(owner?.resetPaths === undefined ? {} : { resetPaths: [...owner.resetPaths] }),
|
|
83
108
|
...(Object.keys(metadata).length ? { metadata } : {}),
|
|
84
109
|
};
|
|
85
110
|
}
|
|
@@ -171,9 +196,14 @@ function scalar(value) {
|
|
|
171
196
|
* @returns {boolean} 是否可由控件表示 / Whether the control can represent the value.
|
|
172
197
|
*/
|
|
173
198
|
function validValueShape(field, value) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
199
|
+
switch (field.type) {
|
|
200
|
+
case "array":
|
|
201
|
+
if (!Array.isArray(value) || value.some(item => !scalar(item)) || new Set(value).size !== value.length) return false;
|
|
202
|
+
break;
|
|
203
|
+
default:
|
|
204
|
+
if (typeof value !== field.type || !scalar(value)) return false;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
177
207
|
if (field.control === "url" && (!/^[a-z][a-z\d+.-]*:/i.test(value) || /^(?:javascript|data|vbscript):/i.test(value))) return false;
|
|
178
208
|
return true;
|
|
179
209
|
}
|
|
@@ -203,7 +233,7 @@ export function resolveStoredValue(field, stored) {
|
|
|
203
233
|
if (value === undefined) return {};
|
|
204
234
|
if (!validValueShape(field, value)) {
|
|
205
235
|
return {
|
|
206
|
-
...(Object.
|
|
236
|
+
...(Object.prototype.hasOwnProperty.call(field, "defaultValue") ? { value: field.defaultValue } : {}),
|
|
207
237
|
warning: { kind: "invalid-value", values: [stored] },
|
|
208
238
|
};
|
|
209
239
|
}
|
package/src/browser/client.d.mts
CHANGED
|
@@ -66,9 +66,9 @@ export class PreferencesClient {
|
|
|
66
66
|
readSettings(): Promise<unknown>;
|
|
67
67
|
/** 读取 Caches 子树 / Read the Caches subtree. */
|
|
68
68
|
readCaches(): Promise<unknown>;
|
|
69
|
-
/**
|
|
69
|
+
/** 删除声明的 Caches 子树 / Delete the declared Caches subtree. */
|
|
70
70
|
clearCaches(): Promise<void>;
|
|
71
|
-
/**
|
|
71
|
+
/** 删除声明的设置路径,默认整个模块数据,并恢复默认值 / Delete declared setting paths, defaulting to module data, and restore defaults. */
|
|
72
72
|
reset(): Promise<void>;
|
|
73
73
|
/** 取消请求 / Cancel requests. */
|
|
74
74
|
leave(): void;
|