@nsnanocat/preference-panes 1.3.0 → 1.3.1

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.
@@ -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, requestConfirmation, requestURLNavigation, settingRow, statusView } from "./components.mjs";
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, navigation, operation queue, and transient notifications.
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
- let back, heading, menu;
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
- // 嵌入模式向宿主发布导航状态,宿主不读取或修改模块内部 DOM。
79
- // Embedded mode publishes navigation state without host reads or mutations of the module DOM.
80
- const publishNavigation = () => {
81
- const actions = handlers.size ? menuItems : [];
82
- if (frame)
83
- frame.dispatchEvent(
84
- new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:change", {
85
- detail: { title: currentTitle, module: definition.module, busy: saving, canGoBack, actions },
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 onAction = event => {
95
- if (!saving && handlers.has(event.detail)) runAction(event.detail);
76
+ const publishNavigation = () => {
77
+ if (!destroyed) page.update();
96
78
  };
97
- frame?.addEventListener("preferencepanes:action", onAction);
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
- // 宿主接管时不创建网页 Toast,也不运行其计时器。
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
- * Share async error handling between both menus, including host-dialog errors.
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
- const editors = new Map();
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
- if (requestURLNavigation(window, link.href)) event.preventDefault();
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 page = node("section", "pp-choice-page");
322
- if (field.description) page.append(node("p", "pp-description", 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
- page.append(choices);
271
+ editor.append(choices);
325
272
  inputContainer = choices;
326
- editors.set(field.key, { node: page, title: match?.[2] ?? field.name });
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 = () => navigation.open(field.key);
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
- editors.set("$settings", { node: settingsPage, title: "设置" });
466
- handlers.set("viewSettings", () => {
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
- navigation.open("$settings");
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
- editors.set("$caches", { node: cachePage, title: "缓存" });
490
- handlers.set("viewCaches", () => {
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
- navigation.open("$caches");
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 requestConfirmation(window, message)) || destroyed || saving) return;
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 requestConfirmation(window, message)) || destroyed || saving) return;
484
+ if (!(await page.confirm(message)) || destroyed || saving) return;
524
485
  return perform(() => client.reset(), controls);
525
486
  });
526
- navigation?.destroy();
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
- updateNavigation();
489
+ page.refresh();
531
490
  }
532
- /**
533
- * 已加载的表单交由导航组件返回;加载阶段可以返回先前文档。
534
- * Loaded forms delegate back to navigation; loading views can return to the previous document.
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
- if (active && !saving) client.leave();
551
- clearTimeout(timer);
497
+ client.leave();
552
498
  shell.remove();
553
499
  };
554
500
  }
555
501
 
556
502
  /**
557
- * 移除监听器、定时器、会话和挂载内容。
558
- * Remove listeners, timers, session, and mounted content.
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() {