@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.
- package/CHANGELOG.md +13 -0
- package/README.md +19 -35
- package/dist/home.html +1638 -462
- package/dist/{module/navigation.mjs → navigation.mjs} +51 -143
- package/dist/preference-panes.mjs +856 -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 +325 -0
- package/src/browser/panel.mjs +99 -123
- package/src/browser/styles.css +133 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* 共用菜单按钮和底部操作菜单;原生宿主可通过 MORE 调用。
|
|
3
|
+
* Shared menu trigger and bottom action sheet; native hosts may invoke it through MORE.
|
|
4
4
|
*/
|
|
5
5
|
class ActionMenu {
|
|
6
6
|
#button;
|
|
@@ -28,13 +28,14 @@ class ActionMenu {
|
|
|
28
28
|
this.element = document.createElement("span");
|
|
29
29
|
const triggerRoot = this.element.attachShadow({ mode: "open" });
|
|
30
30
|
triggerRoot.innerHTML = `<style>
|
|
31
|
-
:host{display:inline-flex;width:
|
|
31
|
+
:host{display:inline-flex;flex:none;width:37px;height:37px;color:inherit}
|
|
32
32
|
:host([hidden]){display:none!important}
|
|
33
|
-
button{width:
|
|
33
|
+
button{box-sizing:border-box;width:37px;height:37px;padding:8px;border-radius:4px;font:inherit;cursor:pointer;border:0;color:inherit;background:none;transition:background-color .3s}
|
|
34
|
+
button:hover{background:var(--pp-field,#8882)}
|
|
34
35
|
button:disabled{opacity:.4;cursor:default}
|
|
35
36
|
button:focus-visible{outline:2px solid currentColor;outline-offset:-3px}
|
|
36
|
-
svg{display:block;width:
|
|
37
|
-
</style><button type="button" aria-label="更多操作" aria-haspopup="menu" aria-expanded="false"><svg viewBox="0 0
|
|
37
|
+
svg{display:block;width:21px;height:21px;fill:none}
|
|
38
|
+
</style><button type="button" aria-label="更多操作" aria-haspopup="menu" aria-expanded="false"><svg viewBox="0 0 21 21" aria-hidden="true"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.667" d="M3.645 5.225h13.333M3.645 10.225h13.333M3.645 15.225h13.333"/></svg></button>`;
|
|
38
39
|
this.#button = triggerRoot.querySelector("button");
|
|
39
40
|
this.#layer = document.createElement("span");
|
|
40
41
|
const layerRoot = this.#layer.attachShadow({ mode: "open" });
|
|
@@ -146,129 +147,6 @@ class ActionMenu {
|
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
/**
|
|
150
|
-
* 模块文档容器:获取静态 HTML,并通过 iframe 数据属性传递页面资源输入。
|
|
151
|
-
* Module document container: fetch static HTML and pass page resource inputs through iframe data attributes.
|
|
152
|
-
*/
|
|
153
|
-
class ModuleFrame extends EventTarget {
|
|
154
|
-
#url;
|
|
155
|
-
#options;
|
|
156
|
-
#controller = new AbortController();
|
|
157
|
-
#abort = () => this.destroy();
|
|
158
|
-
#state;
|
|
159
|
-
#change = event => {
|
|
160
|
-
this.#state = { ...event.detail, actions: event.detail.actions ?? [] };
|
|
161
|
-
this.dispatchEvent(new Event("change"));
|
|
162
|
-
};
|
|
163
|
-
#forward(type, event) {
|
|
164
|
-
if (!this.dispatchEvent(new CustomEvent(type, { cancelable: true, detail: event.detail }))) event.preventDefault();
|
|
165
|
-
}
|
|
166
|
-
#confirmation = event => this.#forward("confirm", event);
|
|
167
|
-
#notice = event => this.#forward("notice", event);
|
|
168
|
-
#openURL = event => this.#forward("open-url", event);
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* 建立 iframe;调用方挂载 element 后调用 load。
|
|
172
|
-
* Create the iframe; callers mount element and then call load.
|
|
173
|
-
* @param {string | URL} url 模块请求地址 / Module request URL.
|
|
174
|
-
* @param {{signal?: AbortSignal, headers?: HeadersInit}} [options] 请求选项 / Request options.
|
|
175
|
-
*/
|
|
176
|
-
constructor(url, options = {}) {
|
|
177
|
-
super();
|
|
178
|
-
this.#url = new URL(url, document.baseURI);
|
|
179
|
-
const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#url.pathname);
|
|
180
|
-
if (!match) throw new TypeError("Open a concrete module URL");
|
|
181
|
-
this.#options = { signal: options.signal };
|
|
182
|
-
this.element = document.createElement("iframe");
|
|
183
|
-
this.element.title = `${match[1]} 设置`;
|
|
184
|
-
this.element.dataset.preferencePanes = "true";
|
|
185
|
-
this.element.dataset.preferencePanesModule = match[1];
|
|
186
|
-
this.element.dataset.preferencePanesBase = this.#url.href;
|
|
187
|
-
const headers = new Headers(options.headers);
|
|
188
|
-
for (const [header, property, kind] of [
|
|
189
|
-
["X-PreferencePanes-JSON", "preferencePanesJson", "BoxJS"],
|
|
190
|
-
["X-PreferencePanes-CSS", "preferencePanesStylesheet", "CSS"],
|
|
191
|
-
]) {
|
|
192
|
-
if (!headers.has(header)) continue;
|
|
193
|
-
const source = headers.get(header)?.trim();
|
|
194
|
-
if (!source) {
|
|
195
|
-
if (kind === "BoxJS") throw new TypeError("BoxJS resource URL is required");
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
const resource = new URL(source, this.#url);
|
|
199
|
-
if (!["http:", "https:"].includes(resource.protocol)) throw new TypeError(`${kind} resource must use HTTP(S)`);
|
|
200
|
-
this.element.dataset[property] = resource.href;
|
|
201
|
-
}
|
|
202
|
-
this.element.addEventListener("preferencepanes:change", this.#change);
|
|
203
|
-
this.element.addEventListener("preferencepanes:confirm", this.#confirmation);
|
|
204
|
-
this.element.addEventListener("preferencepanes:notice", this.#notice);
|
|
205
|
-
this.element.addEventListener("preferencepanes:open-url", this.#openURL);
|
|
206
|
-
this.#state = { title: match[1], module: match[1], busy: false, canGoBack: true, actions: [] };
|
|
207
|
-
options.signal?.addEventListener("abort", this.#abort, { once: true });
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* 当前模块导航状态。
|
|
212
|
-
* Current module navigation state.
|
|
213
|
-
*/
|
|
214
|
-
get state() {
|
|
215
|
-
return { ...this.#state };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* 获取原始 HTML;晚到响应在退出后不得重新挂载。
|
|
220
|
-
* Fetch unmodified HTML; a late response must not remount after departure.
|
|
221
|
-
* @returns {Promise<void>} HTML 已交给 iframe;表单状态通过 change 事件提供 / HTML assigned; form state is reported through change.
|
|
222
|
-
*/
|
|
223
|
-
async load() {
|
|
224
|
-
if (this.#options.signal?.aborted) this.destroy();
|
|
225
|
-
const timer = setTimeout(() => this.#controller.abort(), 10000);
|
|
226
|
-
try {
|
|
227
|
-
const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit", signal: this.#controller.signal });
|
|
228
|
-
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
|
|
229
|
-
const html = await response.text();
|
|
230
|
-
if (this.#controller.signal.aborted) throw new DOMException("The operation was aborted", "AbortError");
|
|
231
|
-
this.element.srcdoc = html;
|
|
232
|
-
} finally {
|
|
233
|
-
clearTimeout(timer);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* 使用 iframe 的联合历史返回;写入期间不导航。
|
|
239
|
-
* Navigate joint iframe history back, except while a write is pending.
|
|
240
|
-
* @returns {void} 无返回值 / No return value.
|
|
241
|
-
*/
|
|
242
|
-
back() {
|
|
243
|
-
if (!this.#state.busy && this.#state.canGoBack) this.element.contentWindow.history.back();
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* 向模块发送菜单操作,不让宿主访问内部 DOM 或存储客户端。
|
|
248
|
-
* Dispatch a menu action without host access to internal DOM or the storage client.
|
|
249
|
-
* @param {string} id 当前可用操作 / Available action identifier.
|
|
250
|
-
* @returns {void} 无返回值 / No return value.
|
|
251
|
-
*/
|
|
252
|
-
perform(id) {
|
|
253
|
-
if (this.#state.busy || !this.#state.actions.some(action => action.id === id)) throw new Error("Action is not available");
|
|
254
|
-
this.element.dispatchEvent(new CustomEvent("preferencepanes:action", { detail: id }));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* 取消加载与事件订阅;节点保留到 Navigation 的退出动画结束。
|
|
259
|
-
* Cancel loading and subscriptions; Navigation retains the node until its exit animation ends.
|
|
260
|
-
* @returns {void} 无返回值 / No return value.
|
|
261
|
-
*/
|
|
262
|
-
destroy() {
|
|
263
|
-
this.#controller.abort();
|
|
264
|
-
this.#options.signal?.removeEventListener("abort", this.#abort);
|
|
265
|
-
this.element.removeEventListener("preferencepanes:change", this.#change);
|
|
266
|
-
this.element.removeEventListener("preferencepanes:confirm", this.#confirmation);
|
|
267
|
-
this.element.removeEventListener("preferencepanes:notice", this.#notice);
|
|
268
|
-
this.element.removeEventListener("preferencepanes:open-url", this.#openURL);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
150
|
/**
|
|
273
151
|
* 模块探测请求选项。
|
|
274
152
|
* Options for a module probe request.
|
|
@@ -395,8 +273,8 @@ class ModuleStatus extends EventTarget {
|
|
|
395
273
|
}
|
|
396
274
|
|
|
397
275
|
/**
|
|
398
|
-
*
|
|
399
|
-
* Navigate
|
|
276
|
+
* 同一文档内的首页、模块及字段子页导航。
|
|
277
|
+
* Navigate homepage, module and field views within one document.
|
|
400
278
|
*/
|
|
401
279
|
class Navigation extends EventTarget {
|
|
402
280
|
#container;
|
|
@@ -408,7 +286,7 @@ class Navigation extends EventTarget {
|
|
|
408
286
|
#retiring;
|
|
409
287
|
#controller;
|
|
410
288
|
#animation;
|
|
411
|
-
#scroll = new
|
|
289
|
+
#scroll = new Map();
|
|
412
290
|
#onHistory = () => this.#route();
|
|
413
291
|
#onPageShow = event => {
|
|
414
292
|
if (event.persisted) this.#route(true);
|
|
@@ -451,28 +329,37 @@ class Navigation extends EventTarget {
|
|
|
451
329
|
}
|
|
452
330
|
|
|
453
331
|
/**
|
|
454
|
-
*
|
|
455
|
-
* Push a
|
|
332
|
+
* 加入模块或字段子页历史,保留路由中的层级分隔符。
|
|
333
|
+
* Push a module or field view while retaining route hierarchy separators.
|
|
456
334
|
* @param {string} key 子页键 / Detail key.
|
|
457
335
|
* @returns {void} 无返回值 / No return value.
|
|
458
336
|
*/
|
|
459
337
|
open(key) {
|
|
460
338
|
if (key === this.#key) return;
|
|
461
339
|
const url = new URL(this.#window.location.href);
|
|
462
|
-
url.hash = encodeURIComponent(
|
|
340
|
+
url.hash = key.split("/").map(encodeURIComponent).join("/");
|
|
463
341
|
this.#window.history.pushState({ ...this.#window.history.state, preferencePanesRoute: key }, "", url.href);
|
|
464
342
|
this.#route();
|
|
465
343
|
}
|
|
466
344
|
|
|
467
345
|
/**
|
|
468
|
-
*
|
|
469
|
-
* Go back through
|
|
346
|
+
* 沿文档历史返回,根页可退回上个文档。
|
|
347
|
+
* Go back through document history, including the previous document from home.
|
|
470
348
|
* @returns {void} 无返回值 / No return value.
|
|
471
349
|
*/
|
|
472
350
|
back() {
|
|
473
351
|
if (this.canGoBack) this.#window.history.back();
|
|
474
352
|
}
|
|
475
353
|
|
|
354
|
+
/**
|
|
355
|
+
* 更新当前页的挂载视图,保留路由和滚动位置。
|
|
356
|
+
* Refresh the mounted view while retaining route and scroll position.
|
|
357
|
+
* @returns {void} 无返回值 / No return value.
|
|
358
|
+
*/
|
|
359
|
+
refresh() {
|
|
360
|
+
this.#route(true);
|
|
361
|
+
}
|
|
362
|
+
|
|
476
363
|
/**
|
|
477
364
|
* 解析 URL 并统一处理页面切换、加载取消与动画结束后的释放。
|
|
478
365
|
* Resolve the URL and coordinate transitions, cancellation and release after animation.
|
|
@@ -489,28 +376,42 @@ class Navigation extends EventTarget {
|
|
|
489
376
|
key = "";
|
|
490
377
|
}
|
|
491
378
|
if (!reload && key === this.#key) return;
|
|
379
|
+
if (key !== this.#key) this.#scroll.set(this.#key, (this.#view ?? this.#home).scrollTop);
|
|
492
380
|
this.#controller?.abort();
|
|
493
381
|
this.#controller = new AbortController();
|
|
494
382
|
const next = key ? this.#create(key, this.#controller.signal) : undefined;
|
|
495
383
|
if (!next) key = "";
|
|
496
384
|
const history = this.#window.history;
|
|
497
|
-
//
|
|
498
|
-
// Seed
|
|
385
|
+
// 直接打开字段页时补齐首页和模块历史;刷新不重复堆叠。
|
|
386
|
+
// Seed homepage and module history for direct field links without stacking on reload.
|
|
499
387
|
if (url.hash && history.state?.preferencePanesRoute !== key) {
|
|
500
388
|
url.hash = "";
|
|
501
389
|
history.replaceState({ ...history.state, preferencePanesRoute: "" }, "", url.href);
|
|
502
390
|
if (key) {
|
|
503
|
-
|
|
391
|
+
const module = key.split("/")[0];
|
|
392
|
+
if (key.includes("/") && module !== "$link") {
|
|
393
|
+
url.hash = encodeURIComponent(module);
|
|
394
|
+
history.pushState({ ...history.state, preferencePanesRoute: module }, "", url.href);
|
|
395
|
+
}
|
|
396
|
+
url.hash = key.split("/").map(encodeURIComponent).join("/");
|
|
504
397
|
history.pushState({ ...history.state, preferencePanesRoute: key }, "", url.href);
|
|
505
398
|
}
|
|
506
399
|
}
|
|
507
400
|
const previous = this.#view;
|
|
401
|
+
if (previous === next && this.#key === key) {
|
|
402
|
+
if (previous && this.#scroll.has(key)) {
|
|
403
|
+
previous.scrollTop = this.#scroll.get(key);
|
|
404
|
+
this.#scroll.delete(key);
|
|
405
|
+
}
|
|
406
|
+
this.dispatchEvent(new Event("change"));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const scroll = this.#scroll.get(key) ?? (key === this.#key ? (previous?.scrollTop ?? 0) : 0);
|
|
508
410
|
const position = previous ? this.#window.getComputedStyle(previous).transform : "none";
|
|
509
411
|
this.#animation?.cancel();
|
|
510
412
|
this.#retiring?.remove();
|
|
511
413
|
this.#retiring = previous;
|
|
512
414
|
if (previous) {
|
|
513
|
-
this.#scroll.set(previous, previous.scrollTop);
|
|
514
415
|
previous.inert = true;
|
|
515
416
|
}
|
|
516
417
|
this.#key = key;
|
|
@@ -519,7 +420,14 @@ class Navigation extends EventTarget {
|
|
|
519
420
|
if (next) {
|
|
520
421
|
next.inert = false;
|
|
521
422
|
this.#container.append(next);
|
|
522
|
-
next.scrollTop =
|
|
423
|
+
next.scrollTop = scroll;
|
|
424
|
+
// 加载占位页可能无法滚到原位置;配置与控件就绪后的 refresh 再恢复一次。
|
|
425
|
+
// A loading placeholder may clamp scrolling; restore once more when config and controls refresh.
|
|
426
|
+
if (next.scrollTop === scroll) this.#scroll.delete(key);
|
|
427
|
+
}
|
|
428
|
+
if (!next) {
|
|
429
|
+
this.#home.scrollTop = this.#scroll.get("") ?? 0;
|
|
430
|
+
this.#scroll.delete("");
|
|
523
431
|
}
|
|
524
432
|
const moving = next ?? previous;
|
|
525
433
|
if (moving) {
|
|
@@ -553,4 +461,4 @@ class Navigation extends EventTarget {
|
|
|
553
461
|
}
|
|
554
462
|
}
|
|
555
463
|
|
|
556
|
-
export { ActionMenu,
|
|
464
|
+
export { ActionMenu, ModuleStatus, Navigation, probeModule };
|