@nsnanocat/preference-panes 1.3.0 → 1.3.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/CHANGELOG.md +20 -0
- package/README.md +21 -33
- package/dist/home.html +1637 -462
- package/dist/{module/navigation.mjs → navigation.mjs} +51 -143
- package/dist/preference-panes.mjs +855 -610
- package/package.json +2 -2
- package/src/browser/ActionMenu.mjs +7 -6
- package/src/browser/Navigation.d.mts +4 -95
- package/src/browser/Navigation.mjs +43 -14
- package/src/browser/client.d.mts +1 -1
- package/src/browser/client.mjs +6 -4
- package/src/browser/components.mjs +0 -44
- package/src/browser/home.d.mts +13 -1
- package/src/browser/home.html +0 -1
- package/src/browser/home.mjs +29 -183
- package/src/browser/mount.mjs +50 -91
- package/src/browser/page.mjs +324 -0
- package/src/browser/panel.mjs +99 -123
- package/src/browser/styles.css +150 -125
- package/dist/module/index.html +0 -15
- package/dist/module/index.mjs +0 -1678
- package/src/browser/ModuleFrame.mjs +0 -122
- package/src/browser/index.mjs +0 -89
- package/src/browser/module.html +0 -15
package/src/browser/panel.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { ActionMenu } from "./ActionMenu.mjs";
|
|
2
1
|
import { validValue } from "./boxjs.mjs";
|
|
3
2
|
import { PreferencesClient } from "./client.mjs";
|
|
4
|
-
import { fieldControl, element as node,
|
|
5
|
-
import { Navigation } from "./Navigation.mjs";
|
|
3
|
+
import { fieldControl, element as node, settingRow, statusView } from "./components.mjs";
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
6
|
* 将字段级存储诊断转换为简短提示。
|
|
@@ -21,20 +19,24 @@ function warningMessage(warning) {
|
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
/**
|
|
24
|
-
*
|
|
25
|
-
* Manage the module form,
|
|
22
|
+
* 管理模块表单、操作队列和设置会话。
|
|
23
|
+
* Manage the module form, operation queue and settings session.
|
|
26
24
|
*/
|
|
27
25
|
export class PreferencesPanel {
|
|
28
26
|
#release;
|
|
27
|
+
#view;
|
|
28
|
+
#state;
|
|
29
|
+
#perform;
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* 挂载 BoxJS 定义对应的模块表单。
|
|
32
33
|
* Mount the module form described by a BoxJS definition.
|
|
33
34
|
* @param {HTMLElement} root 包内挂载元素 / Internal mount element.
|
|
34
35
|
* @param {import("../index.js").ModuleDefinition} definition 已规范化字段定义 / Normalized field definition.
|
|
36
|
+
* @param {import("./page.mjs").PageController} page 内部页面控制器 / Internal page controller.
|
|
35
37
|
*/
|
|
36
|
-
constructor(root, definition) {
|
|
37
|
-
this.#release = this.#mount(root, definition);
|
|
38
|
+
constructor(root, definition, page) {
|
|
39
|
+
this.#release = this.#mount(root, definition, page);
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
/**
|
|
@@ -42,13 +44,13 @@ export class PreferencesPanel {
|
|
|
42
44
|
* Build panel DOM, interactions, and session, then return its release operation.
|
|
43
45
|
* @param {HTMLElement} root 包内挂载元素 / Internal mount element.
|
|
44
46
|
* @param {import("../index.js").ModuleDefinition} definition 已规范化字段定义 / Normalized field definition.
|
|
47
|
+
* @param {import("./page.mjs").PageController} page 内部页面控制器 / Internal page controller.
|
|
45
48
|
* @returns {() => void} 释放操作 / Release operation.
|
|
46
49
|
*/
|
|
47
|
-
#mount(root, definition) {
|
|
50
|
+
#mount(root, definition, page) {
|
|
48
51
|
const title = definition.metadata?.name ?? definition.module;
|
|
49
52
|
const document = root.ownerDocument;
|
|
50
53
|
const window = document.defaultView;
|
|
51
|
-
const frame = window.frameElement?.dataset.preferencePanes ? window.frameElement : undefined;
|
|
52
54
|
const shell = node("div", "pp-panel");
|
|
53
55
|
shell.dataset.module = definition.module;
|
|
54
56
|
const handlers = new Map();
|
|
@@ -59,50 +61,25 @@ export class PreferencesPanel {
|
|
|
59
61
|
{ id: "reset", label: "重置设置", destructive: true },
|
|
60
62
|
];
|
|
61
63
|
const viewport = node("div", "pp-viewport");
|
|
62
|
-
|
|
63
|
-
if (frame) shell.append(viewport);
|
|
64
|
-
else {
|
|
65
|
-
const header = node("header", "pp-header");
|
|
66
|
-
back = node("button", "pp-back", "‹");
|
|
67
|
-
back.setAttribute("aria-label", "返回");
|
|
68
|
-
back.type = "button";
|
|
69
|
-
heading = node("h1", "pp-title", title);
|
|
70
|
-
menu = new ActionMenu(id => runAction(id));
|
|
71
|
-
const trailing = node("span", "pp-nav-spacer");
|
|
72
|
-
trailing.append(menu.element);
|
|
73
|
-
header.append(back, heading, trailing);
|
|
74
|
-
shell.append(header, viewport);
|
|
75
|
-
}
|
|
76
|
-
let toast;
|
|
64
|
+
shell.append(viewport);
|
|
77
65
|
root.append(shell);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
);
|
|
88
|
-
else {
|
|
89
|
-
heading.textContent = currentTitle;
|
|
90
|
-
back.disabled = !canGoBack;
|
|
91
|
-
menu.update(actions, saving);
|
|
92
|
-
}
|
|
66
|
+
const editors = new Map();
|
|
67
|
+
this.#view = key => {
|
|
68
|
+
const editor = editors.get(key);
|
|
69
|
+
editor?.load?.();
|
|
70
|
+
return editor?.node ?? root;
|
|
71
|
+
};
|
|
72
|
+
this.#state = key => ({ title: editors.get(key)?.title ?? title, actions: handlers.size ? menuItems : [], busy: saving });
|
|
73
|
+
this.#perform = id => {
|
|
74
|
+
if (!saving && handlers.has(id)) runAction(id);
|
|
93
75
|
};
|
|
94
|
-
const
|
|
95
|
-
if (!
|
|
76
|
+
const publishNavigation = () => {
|
|
77
|
+
if (!destroyed) page.update();
|
|
96
78
|
};
|
|
97
|
-
|
|
98
|
-
let timer,
|
|
99
|
-
navigation,
|
|
100
|
-
generation = 0,
|
|
79
|
+
let generation = 0,
|
|
101
80
|
active = null,
|
|
102
81
|
saving = false,
|
|
103
|
-
destroyed = false
|
|
104
|
-
currentTitle = title,
|
|
105
|
-
canGoBack = window.history.length > 1;
|
|
82
|
+
destroyed = false;
|
|
106
83
|
/**
|
|
107
84
|
* 展示短暂通知,不刷新设置数据。
|
|
108
85
|
* Display a transient notification without refreshing settings.
|
|
@@ -129,26 +106,12 @@ export class PreferencesPanel {
|
|
|
129
106
|
message = "修改成功";
|
|
130
107
|
break;
|
|
131
108
|
}
|
|
132
|
-
|
|
133
|
-
// A host-owned notice creates no web Toast and starts no local timer.
|
|
134
|
-
if (frame && !frame.dispatchEvent(new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:notice", { cancelable: true, detail: { kind: event.kind, message } }))) return;
|
|
135
|
-
if (!toast) {
|
|
136
|
-
toast = node("div", "pp-toast");
|
|
137
|
-
toast.setAttribute("role", "status");
|
|
138
|
-
shell.append(toast);
|
|
139
|
-
}
|
|
140
|
-
toast.textContent = message;
|
|
141
|
-
toast.dataset.kind = event.kind;
|
|
142
|
-
toast.hidden = false;
|
|
143
|
-
clearTimeout(timer);
|
|
144
|
-
timer = setTimeout(() => {
|
|
145
|
-
toast.hidden = true;
|
|
146
|
-
}, 2400);
|
|
109
|
+
page.notice({ kind: event.kind, message });
|
|
147
110
|
};
|
|
148
111
|
const client = new PreferencesClient({ definition, notify });
|
|
149
112
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
113
|
+
* 菜单操作统一处理异步错误,包含宿主确认框错误。
|
|
114
|
+
* Handle asynchronous action errors, including host-dialog errors.
|
|
152
115
|
* @param {string} id 操作标识 / Action identifier.
|
|
153
116
|
* @returns {Promise<void>} 操作已处理 / Action handled.
|
|
154
117
|
*/
|
|
@@ -168,8 +131,6 @@ export class PreferencesPanel {
|
|
|
168
131
|
async function open(module) {
|
|
169
132
|
const version = ++generation;
|
|
170
133
|
active = module;
|
|
171
|
-
currentTitle = module;
|
|
172
|
-
canGoBack = window.history.length > 1;
|
|
173
134
|
publishNavigation();
|
|
174
135
|
viewport.replaceChildren(statusView("读取设置…"));
|
|
175
136
|
try {
|
|
@@ -188,7 +149,6 @@ export class PreferencesPanel {
|
|
|
188
149
|
*/
|
|
189
150
|
function controls() {
|
|
190
151
|
const { definition, values } = client.snapshot();
|
|
191
|
-
currentTitle = definition.metadata?.name || active;
|
|
192
152
|
const view = node("section", "pp-fields");
|
|
193
153
|
/**
|
|
194
154
|
* 挂载后执行的多行高度更新
|
|
@@ -196,23 +156,12 @@ export class PreferencesPanel {
|
|
|
196
156
|
* @type {Array<() => void>}
|
|
197
157
|
*/
|
|
198
158
|
const growingInputs = [];
|
|
199
|
-
|
|
159
|
+
editors.clear();
|
|
200
160
|
const summaries = [];
|
|
201
161
|
const warningRefreshers = [];
|
|
202
162
|
const groups = new Map();
|
|
203
163
|
let queue = Promise.resolve(),
|
|
204
164
|
pendingWrites = 0;
|
|
205
|
-
/**
|
|
206
|
-
* 导航组件处理页面切换,表单只更新当前标题与返回按钮。
|
|
207
|
-
* Let navigation own transitions; the form only updates the title and back button.
|
|
208
|
-
* @returns {void} 无返回值 / No return value.
|
|
209
|
-
*/
|
|
210
|
-
const updateNavigation = () => {
|
|
211
|
-
const editor = editors.get(navigation.current);
|
|
212
|
-
currentTitle = editor?.title ?? definition.metadata?.name ?? active;
|
|
213
|
-
canGoBack = !saving && navigation.canGoBack;
|
|
214
|
-
publishNavigation();
|
|
215
|
-
};
|
|
216
165
|
/**
|
|
217
166
|
* 串行执行模块操作,保持输入可编辑。
|
|
218
167
|
* Serialize module actions while keeping inputs editable.
|
|
@@ -224,7 +173,6 @@ export class PreferencesPanel {
|
|
|
224
173
|
function perform(action, success, failure = () => {}) {
|
|
225
174
|
pendingWrites++;
|
|
226
175
|
saving = true;
|
|
227
|
-
canGoBack = false;
|
|
228
176
|
publishNavigation();
|
|
229
177
|
queue = queue
|
|
230
178
|
.then(action)
|
|
@@ -239,8 +187,6 @@ export class PreferencesPanel {
|
|
|
239
187
|
.finally(() => {
|
|
240
188
|
pendingWrites--;
|
|
241
189
|
saving = pendingWrites > 0;
|
|
242
|
-
if (destroyed && !saving) client.leave();
|
|
243
|
-
canGoBack = !saving && navigation.canGoBack;
|
|
244
190
|
publishNavigation();
|
|
245
191
|
});
|
|
246
192
|
return queue;
|
|
@@ -294,7 +240,8 @@ export class PreferencesPanel {
|
|
|
294
240
|
link.href = value;
|
|
295
241
|
};
|
|
296
242
|
link.addEventListener("click", event => {
|
|
297
|
-
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
page.openURL(link.href);
|
|
298
245
|
});
|
|
299
246
|
row.append(link);
|
|
300
247
|
row.addEventListener("click", event => {
|
|
@@ -318,12 +265,12 @@ export class PreferencesPanel {
|
|
|
318
265
|
break;
|
|
319
266
|
}
|
|
320
267
|
case "checkboxes": {
|
|
321
|
-
const
|
|
322
|
-
if (field.description)
|
|
268
|
+
const editor = node("section", "pp-panel pp-detail pp-choice-page");
|
|
269
|
+
if (field.description) editor.append(node("p", "pp-description", field.description));
|
|
323
270
|
const choices = node("div", "pp-rows");
|
|
324
|
-
|
|
271
|
+
editor.append(choices);
|
|
325
272
|
inputContainer = choices;
|
|
326
|
-
editors.set(field.key, { node:
|
|
273
|
+
editors.set(field.key, { node: editor, title: match?.[2] ?? field.name });
|
|
327
274
|
const summary = node("span", "pp-summary");
|
|
328
275
|
const link = node("button", "pp-choice-link");
|
|
329
276
|
link.type = "button";
|
|
@@ -340,7 +287,7 @@ export class PreferencesPanel {
|
|
|
340
287
|
};
|
|
341
288
|
summaries.push(refresh);
|
|
342
289
|
refresh();
|
|
343
|
-
link.onclick = () =>
|
|
290
|
+
link.onclick = () => page.open(field.key);
|
|
344
291
|
row.addEventListener("click", event => {
|
|
345
292
|
if (!link.contains(event.target)) link.click();
|
|
346
293
|
});
|
|
@@ -458,12 +405,12 @@ export class PreferencesPanel {
|
|
|
458
405
|
if (eventName === "input") inputContainer.addEventListener("compositionend", event => event.target.dispatchEvent(new window.Event("input", { bubbles: true })));
|
|
459
406
|
groups.get(group).append(row);
|
|
460
407
|
}
|
|
461
|
-
const settingsPage = node("section", "pp-settings-page");
|
|
408
|
+
const settingsPage = node("section", "pp-panel pp-detail pp-settings-page");
|
|
462
409
|
const settingsOutput = node("pre", "pp-cache");
|
|
463
410
|
settingsOutput.setAttribute("aria-label", "Settings 内容");
|
|
464
411
|
settingsPage.append(settingsOutput);
|
|
465
|
-
|
|
466
|
-
|
|
412
|
+
const loadSettings = () => {
|
|
413
|
+
editors.get("$settings").load = undefined;
|
|
467
414
|
if (saving) return;
|
|
468
415
|
let value;
|
|
469
416
|
return perform(
|
|
@@ -477,17 +424,24 @@ export class PreferencesPanel {
|
|
|
477
424
|
},
|
|
478
425
|
() => {
|
|
479
426
|
settingsOutput.textContent = value === undefined ? "暂无设置" : JSON.stringify(value, null, 2);
|
|
480
|
-
|
|
427
|
+
},
|
|
428
|
+
() => {
|
|
429
|
+
settingsOutput.textContent = "读取设置失败";
|
|
481
430
|
},
|
|
482
431
|
);
|
|
432
|
+
};
|
|
433
|
+
editors.set("$settings", { node: settingsPage, title: "设置", load: loadSettings });
|
|
434
|
+
handlers.set("viewSettings", () => {
|
|
435
|
+
loadSettings();
|
|
436
|
+
page.open("$settings");
|
|
483
437
|
});
|
|
484
|
-
const cachePage = node("section", "pp-cache-page");
|
|
438
|
+
const cachePage = node("section", "pp-panel pp-detail pp-cache-page");
|
|
485
439
|
const output = node("pre", "pp-cache");
|
|
486
|
-
output.textContent = "
|
|
440
|
+
output.textContent = "正在读取缓存…";
|
|
487
441
|
output.setAttribute("aria-label", "Caches 内容");
|
|
488
442
|
cachePage.append(output);
|
|
489
|
-
|
|
490
|
-
|
|
443
|
+
const loadCaches = () => {
|
|
444
|
+
editors.get("$caches").load = undefined;
|
|
491
445
|
if (saving) return;
|
|
492
446
|
let value;
|
|
493
447
|
return perform(
|
|
@@ -501,15 +455,22 @@ export class PreferencesPanel {
|
|
|
501
455
|
},
|
|
502
456
|
() => {
|
|
503
457
|
output.textContent = value === undefined ? "暂无缓存" : JSON.stringify(value, null, 2);
|
|
504
|
-
|
|
458
|
+
},
|
|
459
|
+
() => {
|
|
460
|
+
output.textContent = "读取缓存失败";
|
|
505
461
|
},
|
|
506
462
|
);
|
|
463
|
+
};
|
|
464
|
+
editors.set("$caches", { node: cachePage, title: "缓存", load: loadCaches });
|
|
465
|
+
handlers.set("viewCaches", () => {
|
|
466
|
+
loadCaches();
|
|
467
|
+
page.open("$caches");
|
|
507
468
|
});
|
|
508
469
|
handlers.set("clearCaches", async () => {
|
|
509
470
|
if (saving) return;
|
|
510
471
|
const shared = definition.cachePath && definition.cachePath !== `@${definition.storageKey}.${active}.Caches`;
|
|
511
472
|
const message = shared ? `清空共享缓存 ${definition.cachePath}?使用该缓存的其它模块也会受影响。` : `清空 ${active} 的全部 Caches?`;
|
|
512
|
-
if (!(await
|
|
473
|
+
if (!(await page.confirm(message)) || destroyed || saving) return;
|
|
513
474
|
return perform(
|
|
514
475
|
() => client.clearCaches(),
|
|
515
476
|
() => {
|
|
@@ -520,42 +481,57 @@ export class PreferencesPanel {
|
|
|
520
481
|
handlers.set("reset", async () => {
|
|
521
482
|
if (saving) return;
|
|
522
483
|
const message = definition.resetPaths ? `重置 ${definition.metadata?.name ?? active} 的设置?只删除本页面的设置,保留共享存储中的其它设置和缓存。` : `重置 ${active} 的设置?这将删除该模块的 Settings、Caches 和其它持久化数据。`;
|
|
523
|
-
if (!(await
|
|
484
|
+
if (!(await page.confirm(message)) || destroyed || saving) return;
|
|
524
485
|
return perform(() => client.reset(), controls);
|
|
525
486
|
});
|
|
526
|
-
|
|
527
|
-
navigation = new Navigation(viewport, view, key => editors.get(key)?.node);
|
|
528
|
-
navigation.addEventListener("change", updateNavigation);
|
|
487
|
+
viewport.replaceChildren(view);
|
|
529
488
|
for (const grow of growingInputs) grow();
|
|
530
|
-
|
|
489
|
+
page.refresh();
|
|
531
490
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
* @returns {void} 无返回值 / No return value.
|
|
536
|
-
*/
|
|
537
|
-
if (back)
|
|
538
|
-
back.onclick = () => {
|
|
539
|
-
if (saving) return;
|
|
540
|
-
if (navigation) navigation.back();
|
|
541
|
-
else window.history.back();
|
|
542
|
-
};
|
|
543
|
-
open(definition.module);
|
|
491
|
+
queueMicrotask(() => {
|
|
492
|
+
if (!destroyed) open(definition.module);
|
|
493
|
+
});
|
|
544
494
|
return () => {
|
|
545
495
|
destroyed = true;
|
|
546
|
-
menu?.destroy();
|
|
547
|
-
frame?.removeEventListener("preferencepanes:action", onAction);
|
|
548
|
-
navigation?.destroy();
|
|
549
496
|
generation++;
|
|
550
|
-
|
|
551
|
-
clearTimeout(timer);
|
|
497
|
+
client.leave();
|
|
552
498
|
shell.remove();
|
|
553
499
|
};
|
|
554
500
|
}
|
|
555
501
|
|
|
556
502
|
/**
|
|
557
|
-
*
|
|
558
|
-
*
|
|
503
|
+
* 返回当前会话中的字段或操作页,不再读取配置。
|
|
504
|
+
* Return a field or action view from the current session without fetching configuration.
|
|
505
|
+
* @param {string} key 子页键 / Detail key.
|
|
506
|
+
* @returns {HTMLElement} 当前视图 / Current view.
|
|
507
|
+
*/
|
|
508
|
+
view(key) {
|
|
509
|
+
return this.#view(key);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* 提供当前页标题及菜单状态。
|
|
514
|
+
* Provide the current title and action state.
|
|
515
|
+
* @param {string} key 子页键 / Detail key.
|
|
516
|
+
* @returns {object} 页面状态 / Page state.
|
|
517
|
+
*/
|
|
518
|
+
state(key) {
|
|
519
|
+
return this.#state(key);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* 执行共用菜单中的操作。
|
|
524
|
+
* Perform an action from the shared menu.
|
|
525
|
+
* @param {string} id 操作标识 / Action identifier.
|
|
526
|
+
* @returns {void} 无返回值 / No return value.
|
|
527
|
+
*/
|
|
528
|
+
perform(id) {
|
|
529
|
+
this.#perform(id);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* 释放读取与视图;已开始的写入仍沿原队列完成。
|
|
534
|
+
* Release reads and views while allowing started writes to finish through the existing queue.
|
|
559
535
|
* @returns {void} 无返回值 / No return value.
|
|
560
536
|
*/
|
|
561
537
|
destroy() {
|