@nsnanocat/preference-panes 1.1.0 → 1.1.2
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 +63 -47
- package/dist/api.js +114 -124
- package/dist/module/index.html +1 -1
- package/dist/module/index.mjs +188 -218
- package/dist/module/navigation.mjs +15 -32
- package/dist/preference-panes.mjs +83 -72
- package/dist/web.js +2 -23
- package/package.json +2 -2
- package/src/api.mjs +117 -124
- package/src/browser/ModuleFrame.mjs +13 -13
- package/src/browser/ModuleStatus.mjs +2 -3
- package/src/browser/Navigation.d.mts +2 -4
- package/src/browser/client.d.mts +5 -5
- package/src/browser/client.mjs +59 -30
- package/src/browser/index.d.ts +5 -21
- package/src/browser/index.mjs +16 -48
- package/src/browser/mount.mjs +16 -32
- package/src/browser/panel.mjs +9 -9
- package/src/index.d.ts +2 -24
- package/src/index.mjs +3 -3
- package/src/web.mjs +1 -5
- package/src/build.mjs +0 -20
- package/src/lib/page-inputs.mjs +0 -17
|
@@ -147,26 +147,8 @@ class ActionMenu {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* @param {URL} url 已解析的页面请求地址 / Parsed page request URL.
|
|
153
|
-
* @param {Record<string, string | undefined>} [headers] 请求头,名称不区分大小写 / Case-insensitive request headers.
|
|
154
|
-
* @returns {{url: string, module: string, json: string, css: string}} 页面上下文与两个资源输入 / Page context and two resource inputs.
|
|
155
|
-
*/
|
|
156
|
-
function pageInputs(url, headers = {}) {
|
|
157
|
-
const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(url.pathname);
|
|
158
|
-
if (!match) throw new TypeError("Open a concrete module URL");
|
|
159
|
-
const module = match[1];
|
|
160
|
-
const values = Object.fromEntries(Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value]));
|
|
161
|
-
const json = values["x-preferencepanes-json"] ?? url.searchParams.get("json") ?? `/configs/${module}`;
|
|
162
|
-
const css = values["x-preferencepanes-css"] ?? url.searchParams.get("css") ?? "";
|
|
163
|
-
if (!json.trim()) throw new TypeError("JSON resource URL is required");
|
|
164
|
-
return { url: url.href, module, json, css };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* 模块文档容器:原始 HTML 不改写,请求上下文随 iframe 元素传递。
|
|
169
|
-
* Module document container: preserve HTML verbatim and carry request context on the iframe element.
|
|
150
|
+
* 模块文档容器:原始 HTML 不改写,只向 iframe 标记模块身份。
|
|
151
|
+
* Module document container: preserve HTML verbatim and mark only the module identity on the iframe.
|
|
170
152
|
*/
|
|
171
153
|
class ModuleFrame extends EventTarget {
|
|
172
154
|
#url;
|
|
@@ -188,23 +170,25 @@ class ModuleFrame extends EventTarget {
|
|
|
188
170
|
};
|
|
189
171
|
|
|
190
172
|
/**
|
|
191
|
-
* 建立 iframe
|
|
192
|
-
* Create the iframe
|
|
173
|
+
* 建立 iframe;调用方挂载 element 后调用 load。
|
|
174
|
+
* Create the iframe; callers mount element and then call load.
|
|
193
175
|
* @param {string | URL} url 模块请求地址 / Module request URL.
|
|
194
|
-
* @param {
|
|
176
|
+
* @param {{signal?: AbortSignal}} [options] 外部取消信号 / External cancellation signal.
|
|
195
177
|
*/
|
|
196
178
|
constructor(url, options = {}) {
|
|
197
179
|
super();
|
|
198
180
|
this.#url = new URL(url, document.baseURI);
|
|
199
|
-
|
|
200
|
-
|
|
181
|
+
const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#url.pathname);
|
|
182
|
+
if (!match) throw new TypeError("Open a concrete module URL");
|
|
183
|
+
this.#options = options;
|
|
201
184
|
this.element = document.createElement("iframe");
|
|
202
|
-
this.element.title = `${
|
|
203
|
-
this.element.dataset.preferencePanes =
|
|
185
|
+
this.element.title = `${match[1]} 设置`;
|
|
186
|
+
this.element.dataset.preferencePanes = "true";
|
|
187
|
+
this.element.dataset.preferencePanesModule = match[1];
|
|
204
188
|
this.element.addEventListener("preferencepanes:change", this.#change);
|
|
205
189
|
this.element.addEventListener("preferencepanes:confirm", this.#confirmation);
|
|
206
190
|
this.element.addEventListener("preferencepanes:notice", this.#notice);
|
|
207
|
-
this.#state = { title:
|
|
191
|
+
this.#state = { title: match[1], module: match[1], busy: false, canGoBack: true, actions: [] };
|
|
208
192
|
options.signal?.addEventListener("abort", this.#abort, { once: true });
|
|
209
193
|
}
|
|
210
194
|
|
|
@@ -225,7 +209,7 @@ class ModuleFrame extends EventTarget {
|
|
|
225
209
|
if (this.#options.signal?.aborted) this.destroy();
|
|
226
210
|
const timer = setTimeout(() => this.#controller.abort(), 10000);
|
|
227
211
|
try {
|
|
228
|
-
const response = await fetch(this.#url, { cache: "no-store", credentials: "omit",
|
|
212
|
+
const response = await fetch(this.#url, { cache: "no-store", credentials: "omit", signal: this.#controller.signal });
|
|
229
213
|
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
|
|
230
214
|
const html = await response.text();
|
|
231
215
|
this.#controller.signal.throwIfAborted();
|
|
@@ -275,7 +259,6 @@ class ModuleFrame extends EventTarget {
|
|
|
275
259
|
* @typedef {object} ModuleProbeOptions
|
|
276
260
|
* @property {typeof globalThis.fetch} [fetch] 可注入的 fetch / Injectable fetch.
|
|
277
261
|
* @property {AbortSignal} [signal] 外部取消信号 / External cancellation signal.
|
|
278
|
-
* @property {string} [json] BoxJS JSON 来源,将随探测请求头传递 / BoxJS JSON source sent in the probe header.
|
|
279
262
|
* @property {number} [timeout] 超时毫秒数,默认 3500 / Timeout in milliseconds, defaults to 3500.
|
|
280
263
|
*/
|
|
281
264
|
|
|
@@ -286,14 +269,14 @@ class ModuleFrame extends EventTarget {
|
|
|
286
269
|
* @param {ModuleProbeOptions} [options] 请求选项 / Request options.
|
|
287
270
|
* @returns {Promise<Response>} 原始 HTTP 响应,可直接读取 status 和响应头 / Native HTTP response; read status and headers directly.
|
|
288
271
|
*/
|
|
289
|
-
async function probeModule(url, { fetch: request = globalThis.fetch,
|
|
272
|
+
async function probeModule(url, { fetch: request = globalThis.fetch, signal, timeout = 3500 } = {}) {
|
|
290
273
|
const controller = new AbortController();
|
|
291
274
|
const abort = () => controller.abort();
|
|
292
275
|
if (signal?.aborted) abort();
|
|
293
276
|
signal?.addEventListener("abort", abort, { once: true });
|
|
294
277
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
295
278
|
try {
|
|
296
|
-
return await request(url, { method: "HEAD", cache: "no-store", credentials: "omit", signal: controller.signal
|
|
279
|
+
return await request(url, { method: "HEAD", cache: "no-store", credentials: "omit", signal: controller.signal });
|
|
297
280
|
} finally {
|
|
298
281
|
clearTimeout(timer);
|
|
299
282
|
signal?.removeEventListener("abort", abort);
|
|
@@ -45,8 +45,8 @@ function normalizeBoxJs(config, module) {
|
|
|
45
45
|
target.owners.add(app);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
-
if (
|
|
49
|
-
const target =
|
|
48
|
+
if (modules.size !== 1) throw new TypeError("Import BoxJS JSON for exactly one module");
|
|
49
|
+
const target = modules.values().next().value ;
|
|
50
50
|
if (!target) throw new TypeError(`No BoxJS settings for module: ${module}`);
|
|
51
51
|
const metadata = normalizeMetadata(target.owners.size === 1 ? presentation([...target.owners][0]) : {});
|
|
52
52
|
const fields = [];
|
|
@@ -428,28 +428,50 @@ class ActionMenu {
|
|
|
428
428
|
*/
|
|
429
429
|
class PreferencesClient {
|
|
430
430
|
#module;
|
|
431
|
-
#configURL;
|
|
432
431
|
#definition;
|
|
433
432
|
#request;
|
|
434
433
|
#notify;
|
|
435
434
|
#timeout;
|
|
436
435
|
#session = new AbortController();
|
|
437
|
-
#values;
|
|
436
|
+
#values = {};
|
|
438
437
|
#saving = false;
|
|
439
438
|
|
|
440
439
|
/**
|
|
441
|
-
*
|
|
442
|
-
* Create a page client that
|
|
443
|
-
* @param {import("./client.mjs").PreferencesClientOptions} options
|
|
440
|
+
* 创建从 BoxJS 定义读取和持久化设置的页面客户端。
|
|
441
|
+
* Create a page client that reads and persists settings from a BoxJS definition.
|
|
442
|
+
* @param {import("./client.mjs").PreferencesClientOptions} options 字段定义、请求与通知 / Field definition, requests, and notifications.
|
|
444
443
|
*/
|
|
445
|
-
constructor({
|
|
446
|
-
this.#module =
|
|
447
|
-
this.#configURL = model.configURL;
|
|
444
|
+
constructor({ definition, fetch: request = globalThis.fetch.bind(globalThis), notify = () => {}, timeout = 10000 }) {
|
|
445
|
+
this.#module = definition.module;
|
|
448
446
|
this.#definition = definition;
|
|
449
447
|
this.#request = request;
|
|
450
448
|
this.#notify = notify;
|
|
451
449
|
this.#timeout = timeout;
|
|
452
|
-
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* 读取一次 Settings 子树并建立页面值快照。
|
|
454
|
+
* Read the Settings subtree once and establish the page value snapshot.
|
|
455
|
+
* @returns {Promise<import("./client.mjs").ModuleSnapshot>} 页面快照 / Page snapshot.
|
|
456
|
+
*/
|
|
457
|
+
async open() {
|
|
458
|
+
let subtree = await this.readSettings();
|
|
459
|
+
if (subtree === undefined) subtree = {};
|
|
460
|
+
if (typeof subtree === "string") subtree = JSON.parse(subtree);
|
|
461
|
+
if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
|
|
462
|
+
const values = {};
|
|
463
|
+
for (const field of this.#definition.fields) {
|
|
464
|
+
const stored = field.key
|
|
465
|
+
.split(".")
|
|
466
|
+
.slice(this.#definition.settingsPath.length)
|
|
467
|
+
.reduce((parent, part) => Object(parent)[part], subtree);
|
|
468
|
+
const value = normalizeStoredValue(field, stored === undefined ? field.defaultValue : stored);
|
|
469
|
+
if (value === undefined) continue;
|
|
470
|
+
if (!validValue(field, value)) throw new TypeError(`Invalid stored value: ${field.key}`);
|
|
471
|
+
values[field.key] = value;
|
|
472
|
+
}
|
|
473
|
+
this.#values = values;
|
|
474
|
+
return this.snapshot();
|
|
453
475
|
}
|
|
454
476
|
|
|
455
477
|
/**
|
|
@@ -467,7 +489,7 @@ class PreferencesClient {
|
|
|
467
489
|
* @returns {Promise<unknown>} Settings 内容或 undefined / Settings content or undefined.
|
|
468
490
|
*/
|
|
469
491
|
async readSettings() {
|
|
470
|
-
const response = await this.#send("get", {
|
|
492
|
+
const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#definition.settingsPath.join(".")}`);
|
|
471
493
|
return response.status === 404 ? undefined : response.json();
|
|
472
494
|
}
|
|
473
495
|
|
|
@@ -477,7 +499,7 @@ class PreferencesClient {
|
|
|
477
499
|
* @returns {Promise<unknown>} Caches 内容或 undefined / Caches content or undefined.
|
|
478
500
|
*/
|
|
479
501
|
async readCaches() {
|
|
480
|
-
const response = await this.#send("get", {
|
|
502
|
+
const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#module}.Caches`);
|
|
481
503
|
return response.status === 404 ? undefined : response.json();
|
|
482
504
|
}
|
|
483
505
|
|
|
@@ -487,7 +509,7 @@ class PreferencesClient {
|
|
|
487
509
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
488
510
|
*/
|
|
489
511
|
clearCaches() {
|
|
490
|
-
return this.#change("delete", {
|
|
512
|
+
return this.#change("delete", `${this.#module}.Caches`, undefined, "clearCaches");
|
|
491
513
|
}
|
|
492
514
|
|
|
493
515
|
/**
|
|
@@ -496,7 +518,7 @@ class PreferencesClient {
|
|
|
496
518
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
497
519
|
*/
|
|
498
520
|
reset() {
|
|
499
|
-
return this.#change("delete",
|
|
521
|
+
return this.#change("delete", this.#module, undefined, "reset");
|
|
500
522
|
}
|
|
501
523
|
|
|
502
524
|
/**
|
|
@@ -516,7 +538,7 @@ class PreferencesClient {
|
|
|
516
538
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
517
539
|
*/
|
|
518
540
|
set(key, value) {
|
|
519
|
-
return this.#change("set",
|
|
541
|
+
return this.#change("set", key, value, "write");
|
|
520
542
|
}
|
|
521
543
|
|
|
522
544
|
/**
|
|
@@ -526,30 +548,31 @@ class PreferencesClient {
|
|
|
526
548
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
527
549
|
*/
|
|
528
550
|
remove(key) {
|
|
529
|
-
return this.#change("delete",
|
|
551
|
+
return this.#change("delete", key, undefined, "delete");
|
|
530
552
|
}
|
|
531
553
|
|
|
532
554
|
/**
|
|
533
|
-
*
|
|
534
|
-
* Send a
|
|
535
|
-
* @param {"get" | "set" | "delete"} action
|
|
536
|
-
* @param {
|
|
555
|
+
* 向固定存储 API 发送完整路径的 form 动作。
|
|
556
|
+
* Send a complete-path form action to the fixed storage API.
|
|
557
|
+
* @param {"get" | "set" | "delete"} action 存储动作 / Storage action.
|
|
558
|
+
* @param {string} path 完整 @root.path / Complete @root.path.
|
|
559
|
+
* @param {unknown} [value] set 写入值 / Value written by set.
|
|
537
560
|
* @returns {Promise<Response>} 原始响应 / Raw response.
|
|
538
561
|
*/
|
|
539
|
-
async #send(action,
|
|
562
|
+
async #send(action, path, value) {
|
|
540
563
|
const controller = new AbortController();
|
|
541
564
|
const abort = () => controller.abort();
|
|
542
565
|
if (this.#session.signal.aborted) abort();
|
|
543
566
|
this.#session.signal.addEventListener("abort", abort, { once: true });
|
|
544
567
|
const timer = setTimeout(abort, this.#timeout);
|
|
545
568
|
try {
|
|
546
|
-
const response = await this.#request(`/api/${
|
|
569
|
+
const response = await this.#request(`/api/${action}`, {
|
|
547
570
|
method: "POST",
|
|
548
571
|
credentials: "omit",
|
|
549
572
|
cache: "no-store",
|
|
550
573
|
signal: controller.signal,
|
|
551
|
-
headers: { "Content-Type": "application/
|
|
552
|
-
body: JSON.stringify(
|
|
574
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
575
|
+
body: new URLSearchParams([[path, action === "set" ? JSON.stringify(value) : ""]]).toString(),
|
|
553
576
|
});
|
|
554
577
|
if (response.status !== 200 && !(action === "get" && response.status === 404)) throw new Error(`HTTP ${response.status}`);
|
|
555
578
|
return response;
|
|
@@ -563,24 +586,28 @@ class PreferencesClient {
|
|
|
563
586
|
* 执行写入动作;成功后只更新当前页面值。
|
|
564
587
|
* Execute a mutation and update only the current page values after success.
|
|
565
588
|
* @param {"set" | "delete"} action API 动作 / API action.
|
|
566
|
-
* @param {
|
|
589
|
+
* @param {string} key 不含存储根的路径 / Path without the storage root.
|
|
590
|
+
* @param {unknown} value set 写入值 / Value written by set.
|
|
567
591
|
* @param {"write" | "delete" | "clearCaches" | "reset"} operation 通知操作 / Notification operation.
|
|
568
|
-
* @param {string} [key] 字段路径 / Field path.
|
|
569
592
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
570
593
|
*/
|
|
571
|
-
async #change(action,
|
|
594
|
+
async #change(action, key, value, operation) {
|
|
572
595
|
if (this.#saving) throw new Error("A settings write is already in progress");
|
|
573
596
|
this.#saving = true;
|
|
574
597
|
try {
|
|
575
|
-
|
|
598
|
+
let field;
|
|
599
|
+
if (operation === "write" || operation === "delete") {
|
|
600
|
+
field = this.#definition.fields.find(candidate => candidate.key === key);
|
|
601
|
+
if (!field || (operation === "write" && !validValue(field, value))) throw new TypeError("Invalid setting value");
|
|
602
|
+
}
|
|
603
|
+
await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
|
|
576
604
|
switch (operation) {
|
|
577
605
|
case "write":
|
|
578
|
-
this.#values[key] = structuredClone(
|
|
606
|
+
this.#values[key] = structuredClone(value);
|
|
579
607
|
break;
|
|
580
608
|
case "delete": {
|
|
581
|
-
const field = this.#definition.fields.find(candidate => candidate.key === key);
|
|
582
609
|
delete this.#values[key];
|
|
583
|
-
if (
|
|
610
|
+
if (Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
|
|
584
611
|
break;
|
|
585
612
|
}
|
|
586
613
|
case "clearCaches":
|
|
@@ -769,24 +796,23 @@ class PreferencesPanel {
|
|
|
769
796
|
#release;
|
|
770
797
|
|
|
771
798
|
/**
|
|
772
|
-
* 挂载
|
|
773
|
-
* Mount the module form
|
|
799
|
+
* 挂载 BoxJS 定义对应的模块表单。
|
|
800
|
+
* Mount the module form described by a BoxJS definition.
|
|
774
801
|
* @param {HTMLElement} root 包内挂载元素 / Internal mount element.
|
|
775
|
-
* @param {import("../index.js").
|
|
802
|
+
* @param {import("../index.js").ModuleDefinition} definition 已规范化字段定义 / Normalized field definition.
|
|
776
803
|
*/
|
|
777
|
-
constructor(root,
|
|
778
|
-
this.#release = this.#mount(root,
|
|
804
|
+
constructor(root, definition) {
|
|
805
|
+
this.#release = this.#mount(root, definition);
|
|
779
806
|
}
|
|
780
807
|
|
|
781
808
|
/**
|
|
782
809
|
* 建立面板 DOM、交互和会话,并返回其释放操作。
|
|
783
810
|
* Build panel DOM, interactions, and session, then return its release operation.
|
|
784
811
|
* @param {HTMLElement} root 包内挂载元素 / Internal mount element.
|
|
785
|
-
* @param {import("../index.js").
|
|
812
|
+
* @param {import("../index.js").ModuleDefinition} definition 已规范化字段定义 / Normalized field definition.
|
|
786
813
|
* @returns {() => void} 释放操作 / Release operation.
|
|
787
814
|
*/
|
|
788
|
-
#mount(root,
|
|
789
|
-
const { definition } = model;
|
|
815
|
+
#mount(root, definition) {
|
|
790
816
|
const title = definition.metadata?.name ?? definition.module;
|
|
791
817
|
const document = root.ownerDocument;
|
|
792
818
|
const window = document.defaultView;
|
|
@@ -878,7 +904,7 @@ class PreferencesPanel {
|
|
|
878
904
|
toast.hidden = true;
|
|
879
905
|
}, 2400);
|
|
880
906
|
};
|
|
881
|
-
const client = new PreferencesClient({
|
|
907
|
+
const client = new PreferencesClient({ definition, notify });
|
|
882
908
|
/**
|
|
883
909
|
* 两种菜单入口共用异步错误处理,包含宿主确认框错误。
|
|
884
910
|
* Share async error handling between both menus, including host-dialog errors.
|
|
@@ -906,6 +932,7 @@ class PreferencesPanel {
|
|
|
906
932
|
publishNavigation();
|
|
907
933
|
viewport.replaceChildren(statusView("读取设置…"));
|
|
908
934
|
try {
|
|
935
|
+
await client.open();
|
|
909
936
|
if (version === generation) controls();
|
|
910
937
|
} catch (error) {
|
|
911
938
|
if (version !== generation) return;
|
|
@@ -1294,15 +1321,14 @@ function installDefaultStyles(document) {
|
|
|
1294
1321
|
}
|
|
1295
1322
|
|
|
1296
1323
|
/**
|
|
1297
|
-
*
|
|
1298
|
-
* Manage
|
|
1324
|
+
* 管理 BoxJS 规范化、主题同步和面板生命周期。
|
|
1325
|
+
* Manage BoxJS normalization, theme synchronization, and panel lifecycle.
|
|
1299
1326
|
*/
|
|
1300
1327
|
class PreferencesView {
|
|
1301
1328
|
#existing;
|
|
1302
1329
|
#root;
|
|
1303
1330
|
#base;
|
|
1304
1331
|
#ownsBase;
|
|
1305
|
-
#custom;
|
|
1306
1332
|
#previousTitle;
|
|
1307
1333
|
#previousTheme;
|
|
1308
1334
|
#systemTheme;
|
|
@@ -1312,22 +1338,12 @@ class PreferencesView {
|
|
|
1312
1338
|
#panel;
|
|
1313
1339
|
|
|
1314
1340
|
/**
|
|
1315
|
-
*
|
|
1316
|
-
* Mount a settings page from
|
|
1317
|
-
* @param {import("../index.js").
|
|
1318
|
-
* @param {string} [css] 可选 CSS 正文 / Optional module-scoped CSS text.
|
|
1341
|
+
* 使用原始 BoxJS JSON 挂载设置页。
|
|
1342
|
+
* Mount a settings page from raw BoxJS JSON.
|
|
1343
|
+
* @param {import("../index.js").BoxJSInput} boxjs 单模块 BoxJS JSON / Single-module BoxJS JSON.
|
|
1319
1344
|
*/
|
|
1320
|
-
constructor(
|
|
1321
|
-
|
|
1322
|
-
const definition = normalizeBoxJs(model.boxjs, model.module);
|
|
1323
|
-
const values = { ...model.values };
|
|
1324
|
-
for (const field of definition.fields) {
|
|
1325
|
-
if (values[field.key] === undefined) continue;
|
|
1326
|
-
values[field.key] = normalizeStoredValue(field, values[field.key]);
|
|
1327
|
-
if (!validValue(field, values[field.key])) throw new TypeError(`Invalid stored value: ${field.key}`);
|
|
1328
|
-
}
|
|
1329
|
-
for (const field of definition.fields) if (values[field.key] === undefined && Object.hasOwn(field, "defaultValue")) values[field.key] = structuredClone(field.defaultValue);
|
|
1330
|
-
const rendered = { ...model, definition, values };
|
|
1345
|
+
constructor(boxjs) {
|
|
1346
|
+
const definition = normalizeBoxJs(boxjs);
|
|
1331
1347
|
const metadata = definition.metadata ?? {};
|
|
1332
1348
|
const image = metadata.icon || metadata.icons?.[1] || metadata.icons?.[0];
|
|
1333
1349
|
if (image) resourceURL(image);
|
|
@@ -1342,9 +1358,6 @@ class PreferencesView {
|
|
|
1342
1358
|
const styles = installDefaultStyles(document);
|
|
1343
1359
|
this.#base = styles.element;
|
|
1344
1360
|
this.#ownsBase = styles.owned;
|
|
1345
|
-
this.#custom = element("style", "");
|
|
1346
|
-
this.#custom.textContent = css;
|
|
1347
|
-
document.head.append(this.#custom);
|
|
1348
1361
|
this.#previousTitle = document.title;
|
|
1349
1362
|
this.#previousTheme = document.documentElement.dataset.theme;
|
|
1350
1363
|
this.#systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
@@ -1359,7 +1372,7 @@ class PreferencesView {
|
|
|
1359
1372
|
document.title = metadata.name ?? definition.module;
|
|
1360
1373
|
try {
|
|
1361
1374
|
this.#root.replaceChildren();
|
|
1362
|
-
this.#panel = new PreferencesPanel(this.#root,
|
|
1375
|
+
this.#panel = new PreferencesPanel(this.#root, definition);
|
|
1363
1376
|
} catch (error) {
|
|
1364
1377
|
this.destroy();
|
|
1365
1378
|
throw error;
|
|
@@ -1387,7 +1400,6 @@ class PreferencesView {
|
|
|
1387
1400
|
this.#systemTheme.removeEventListener("change", this.#syncAppearance);
|
|
1388
1401
|
this.#panel?.destroy();
|
|
1389
1402
|
if (this.#ownsBase) this.#base.remove();
|
|
1390
|
-
this.#custom.remove();
|
|
1391
1403
|
if (this.#existing) this.#root.replaceChildren();
|
|
1392
1404
|
else this.#root.remove();
|
|
1393
1405
|
document.title = this.#previousTitle;
|
|
@@ -1398,14 +1410,13 @@ class PreferencesView {
|
|
|
1398
1410
|
}
|
|
1399
1411
|
|
|
1400
1412
|
/**
|
|
1401
|
-
*
|
|
1402
|
-
* Mount a settings page from
|
|
1403
|
-
* @param {import("../index.js").
|
|
1404
|
-
* @
|
|
1405
|
-
* @returns {PreferencesView} 模块视图 / Module view.
|
|
1413
|
+
* 使用原始 BoxJS JSON 挂载设置页。
|
|
1414
|
+
* Mount a settings page from raw BoxJS JSON.
|
|
1415
|
+
* @param {import("../index.js").BoxJSInput} boxjs 单模块 BoxJS JSON / Single-module BoxJS JSON.
|
|
1416
|
+
* @returns {import("./index.js").MountedPreferences} 模块视图 / Module view.
|
|
1406
1417
|
*/
|
|
1407
|
-
function mount(
|
|
1408
|
-
return new PreferencesView(
|
|
1418
|
+
function mount(boxjs) {
|
|
1419
|
+
return new PreferencesView(boxjs);
|
|
1409
1420
|
}
|
|
1410
1421
|
|
|
1411
|
-
export {
|
|
1422
|
+
export { mount };
|