@nsnanocat/preference-panes 1.2.3 → 1.2.5
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 +19 -8
- package/dist/module/index.html +2 -1
- package/dist/module/index.mjs +19 -3
- package/dist/module/navigation.mjs +20 -4
- package/package.json +1 -1
- package/src/browser/ModuleFrame.mjs +20 -4
- package/src/browser/Navigation.d.mts +3 -3
- package/src/browser/index.mjs +19 -3
- package/src/browser/module.html +1 -0
- package/dist/web.js +0 -1221
- package/src/web.mjs +0 -88
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
PreferencePanes 提供一个由 BoxJS JSON 驱动的通用设置前端,以及一个独立的代理持久化 API。业务模块直接将自己的 BoxJS JSON Mock 到 `/api/{module}`;通用 `/settings/**` 前端和固定存储 API 分别只需要安装一次。
|
|
4
4
|
|
|
5
|
-
在 Biliverse 中,Enhanced
|
|
5
|
+
在 Biliverse 中,Enhanced 统一映射 PreferencePanes 发布的静态模块 HTML、页面入口和导航组件。Global、Redirect、ADBlock 不重复携带页面资源,它们的设置页仍由同一份通用前端读取各自 BoxJS 后渲染。
|
|
6
6
|
|
|
7
7
|
## 浏览器入口
|
|
8
8
|
|
|
@@ -29,17 +29,17 @@ preferences.destroy();
|
|
|
29
29
|
|
|
30
30
|
前端支持 BoxJS 字段数组、单个 app 和 apps 订阅,并要求输入恰好包含一个模块。模块名、存储根和 Settings 路径都从 `@Root.Module.Settings.key` 字段 ID 推导,不从 app 名称或调用参数补充。
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
默认样式随包内置。静态模块页接受 `json`、`css` 查询参数;嵌入 `ModuleFrame` 时也可通过 `X-PreferencePanes-JSON`、`X-PreferencePanes-CSS` 选项传入两个资源 URL。未指定 JSON 时默认读取 `/api/{module}`,未指定 CSS 时只使用默认样式。项目 stylesheet 位于默认样式之后,可覆盖 `pp-*` 变量和组件规则。页面脚本不识别客户端 User-Agent,也不加载客户端 SDK。
|
|
33
33
|
|
|
34
34
|
## 页面与 API
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Release 分别提供三项可直接映射的页面资源:
|
|
37
37
|
|
|
38
38
|
- `GET /settings/{module}`
|
|
39
39
|
- `GET /settings/assets/index.mjs`
|
|
40
40
|
- `GET /settings/assets/navigation.mjs`
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
模块 HTML、页面入口和导航组件分别对应 Release 的 `index.html`、`index.mjs`、`navigation.mjs`,不得再打包进代理响应脚本。模块页面从 URL 或 `ModuleFrame` 的模块标记取得模块名,读取 BoxJS 资源后调用 `mount(boxjs)`。BoxJS 与可选 CSS 地址必须解析为 HTTP(S);未声明 BoxJS 时回退同源 `/api/{module}`,页面不直接访问 `/configs/**`。
|
|
43
43
|
|
|
44
44
|
业务模块模板直接处理:
|
|
45
45
|
|
|
@@ -76,13 +76,16 @@ await status.check("/api/Module");
|
|
|
76
76
|
|
|
77
77
|
const frame = new ModuleFrame("/settings/Module", {
|
|
78
78
|
signal,
|
|
79
|
-
headers: {
|
|
79
|
+
headers: {
|
|
80
|
+
"X-PreferencePanes-JSON": "https://example.org/Module.boxjs.json",
|
|
81
|
+
"X-PreferencePanes-CSS": "https://example.org/theme.css",
|
|
82
|
+
},
|
|
80
83
|
});
|
|
81
84
|
container.append(frame.element);
|
|
82
85
|
await frame.load();
|
|
83
86
|
```
|
|
84
87
|
|
|
85
|
-
`ModuleFrame` 接受 `{ signal?: AbortSignal; headers?: HeadersInit }
|
|
88
|
+
`ModuleFrame` 接受 `{ signal?: AbortSignal; headers?: HeadersInit }`。`X-PreferencePanes-JSON` 与 `X-PreferencePanes-CSS` 只用于校验并写入 iframe 数据属性,不作为网络请求 Header;静态 HTML 仍以普通 GET 获取并原样设置为 `iframe.srcdoc`。宿主通过 `change`、`confirm`、`notice`、`open-url` 事件同步标题、操作菜单、确认框、提示和链接跳转,不读取或改写 iframe 内部 DOM。项目主页、Bilibili JSBridge、原生导航和视觉样式仍由宿主负责。
|
|
86
89
|
|
|
87
90
|
## 构建与验证
|
|
88
91
|
|
|
@@ -93,8 +96,16 @@ npm run apifox:check
|
|
|
93
96
|
npm pack --dry-run
|
|
94
97
|
```
|
|
95
98
|
|
|
96
|
-
构建生成 `dist/api.js`、`dist/
|
|
99
|
+
构建生成 `dist/api.js`、`dist/preference-panes.mjs` 和 `dist/module/index.html`、`index.mjs`、`navigation.mjs`。包不提供 `web.js`,也不提供按模块生成 HTML/CSS 的公开 `build()`。
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
### 内置演示
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
npm run preview
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
预览入口会自动加载 `examples/Module.boxjs.json`,覆盖 `boolean`、`selects`、`checkboxes`、`text`、`textarea`、`number` 和 `url` 全部受支持控件。顶端 CSS 选择器默认使用包内置 CSS,也可切换 `examples/theme.css` 或导入本地 CSS 文件。预览服务器直接提供正式构建的静态模块页和 JS 文件,通过 `json`、`css` 查询参数传入资源地址,并使用固定存储 API 与独立内存存储,不维护演示专用渲染器。
|
|
108
|
+
|
|
109
|
+
内置存储会提供已移除选项和格式错误值,用于直接核对字段级黄色警告及非阻断加载;URL 字段还会携带同路径旧存储值,验证只读链接始终使用 BoxJS `val`。测试台同时保留上传其它 BoxJS JSON 并生成隔离预览的能力。
|
|
99
110
|
|
|
100
111
|
[接口规范](apifox/Specification.md) · [宿主集成](apifox/HostIntegration.md) · [Apifox JSON](apifox/preference-panes.apifox.json)
|
package/dist/module/index.html
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
6
6
|
<meta name="color-scheme" content="light dark">
|
|
7
|
+
|
|
7
8
|
<title>Module Preferences</title>
|
|
8
9
|
<style>body { margin: 0; }</style>
|
|
9
10
|
|
|
10
11
|
</head>
|
|
11
12
|
<body>
|
|
12
13
|
<main id="preferences"></main>
|
|
13
|
-
<script type="module" src="/settings/assets/index.mjs?v=1.2.
|
|
14
|
+
<script type="module" src="/settings/assets/index.mjs?v=1.2.5"></script>
|
|
14
15
|
</body>
|
|
15
16
|
</html>
|
package/dist/module/index.mjs
CHANGED
|
@@ -1561,8 +1561,8 @@ class ModulePage {
|
|
|
1561
1561
|
}
|
|
1562
1562
|
|
|
1563
1563
|
/**
|
|
1564
|
-
*
|
|
1565
|
-
* Read BoxJS JSON
|
|
1564
|
+
* 读取页面声明的 BoxJS JSON 并挂载通用前端。
|
|
1565
|
+
* Read the BoxJS JSON declared by the page and mount the generic frontend.
|
|
1566
1566
|
* @returns {Promise<void>} 启动完成 / Startup completion.
|
|
1567
1567
|
*/
|
|
1568
1568
|
async start() {
|
|
@@ -1574,7 +1574,23 @@ class ModulePage {
|
|
|
1574
1574
|
const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#window.location.pathname);
|
|
1575
1575
|
const module = embedded ?? match?.[1];
|
|
1576
1576
|
if (!module) throw new TypeError("Open a concrete module URL");
|
|
1577
|
-
const
|
|
1577
|
+
const frame = this.#window.frameElement?.dataset;
|
|
1578
|
+
const base = frame?.preferencePanesBase ?? this.#window.location.href;
|
|
1579
|
+
this.#window.document.querySelector("link[data-preference-panes-stylesheet]")?.remove();
|
|
1580
|
+
const stylesheetSource = frame?.preferencePanesStylesheet ?? new URLSearchParams(this.#window.location.search).get("css")?.trim();
|
|
1581
|
+
if (stylesheetSource) {
|
|
1582
|
+
const stylesheet = new URL(stylesheetSource, base);
|
|
1583
|
+
if (!["http:", "https:"].includes(stylesheet.protocol)) throw new TypeError("CSS resource must use HTTP(S)");
|
|
1584
|
+
const link = this.#window.document.createElement("link");
|
|
1585
|
+
link.dataset.preferencePanesStylesheet = "true";
|
|
1586
|
+
link.rel = "stylesheet";
|
|
1587
|
+
link.href = stylesheet.href;
|
|
1588
|
+
this.#window.document.head.append(link);
|
|
1589
|
+
}
|
|
1590
|
+
const jsonSource = frame?.preferencePanesJson ?? new URLSearchParams(this.#window.location.search).get("json")?.trim();
|
|
1591
|
+
const source = new URL(jsonSource || `/api/${encodeURIComponent(module)}`, base);
|
|
1592
|
+
if (!["http:", "https:"].includes(source.protocol)) throw new TypeError("BoxJS resource must use HTTP(S)");
|
|
1593
|
+
const response = await fetch(source, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
|
|
1578
1594
|
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
|
|
1579
1595
|
const boxjs = await response.json();
|
|
1580
1596
|
normalizeBoxJs(boxjs, module);
|
|
@@ -147,8 +147,8 @@ class ActionMenu {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
151
|
-
* Module document container:
|
|
150
|
+
* 模块文档容器:获取静态 HTML,并通过 iframe 数据属性传递页面资源输入。
|
|
151
|
+
* Module document container: fetch static HTML and pass page resource inputs through iframe data attributes.
|
|
152
152
|
*/
|
|
153
153
|
class ModuleFrame extends EventTarget {
|
|
154
154
|
#url;
|
|
@@ -178,11 +178,27 @@ class ModuleFrame extends EventTarget {
|
|
|
178
178
|
this.#url = new URL(url, document.baseURI);
|
|
179
179
|
const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#url.pathname);
|
|
180
180
|
if (!match) throw new TypeError("Open a concrete module URL");
|
|
181
|
-
this.#options = { signal: options.signal
|
|
181
|
+
this.#options = { signal: options.signal };
|
|
182
182
|
this.element = document.createElement("iframe");
|
|
183
183
|
this.element.title = `${match[1]} 设置`;
|
|
184
184
|
this.element.dataset.preferencePanes = "true";
|
|
185
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
|
+
}
|
|
186
202
|
this.element.addEventListener("preferencepanes:change", this.#change);
|
|
187
203
|
this.element.addEventListener("preferencepanes:confirm", this.#confirmation);
|
|
188
204
|
this.element.addEventListener("preferencepanes:notice", this.#notice);
|
|
@@ -208,7 +224,7 @@ class ModuleFrame extends EventTarget {
|
|
|
208
224
|
if (this.#options.signal?.aborted) this.destroy();
|
|
209
225
|
const timer = setTimeout(() => this.#controller.abort(), 10000);
|
|
210
226
|
try {
|
|
211
|
-
const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit",
|
|
227
|
+
const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit", signal: this.#controller.signal });
|
|
212
228
|
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
|
|
213
229
|
const html = await response.text();
|
|
214
230
|
this.#controller.signal.throwIfAborted();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nsnanocat/preference-panes",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Module document container:
|
|
2
|
+
* 模块文档容器:获取静态 HTML,并通过 iframe 数据属性传递页面资源输入。
|
|
3
|
+
* Module document container: fetch static HTML and pass page resource inputs through iframe data attributes.
|
|
4
4
|
*/
|
|
5
5
|
export class ModuleFrame extends EventTarget {
|
|
6
6
|
#url;
|
|
@@ -30,11 +30,27 @@ export class ModuleFrame extends EventTarget {
|
|
|
30
30
|
this.#url = new URL(url, document.baseURI);
|
|
31
31
|
const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#url.pathname);
|
|
32
32
|
if (!match) throw new TypeError("Open a concrete module URL");
|
|
33
|
-
this.#options = { signal: options.signal
|
|
33
|
+
this.#options = { signal: options.signal };
|
|
34
34
|
this.element = document.createElement("iframe");
|
|
35
35
|
this.element.title = `${match[1]} 设置`;
|
|
36
36
|
this.element.dataset.preferencePanes = "true";
|
|
37
37
|
this.element.dataset.preferencePanesModule = match[1];
|
|
38
|
+
this.element.dataset.preferencePanesBase = this.#url.href;
|
|
39
|
+
const headers = new Headers(options.headers);
|
|
40
|
+
for (const [header, property, kind] of [
|
|
41
|
+
["X-PreferencePanes-JSON", "preferencePanesJson", "BoxJS"],
|
|
42
|
+
["X-PreferencePanes-CSS", "preferencePanesStylesheet", "CSS"],
|
|
43
|
+
]) {
|
|
44
|
+
if (!headers.has(header)) continue;
|
|
45
|
+
const source = headers.get(header)?.trim();
|
|
46
|
+
if (!source) {
|
|
47
|
+
if (kind === "BoxJS") throw new TypeError("BoxJS resource URL is required");
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const resource = new URL(source, this.#url);
|
|
51
|
+
if (!["http:", "https:"].includes(resource.protocol)) throw new TypeError(`${kind} resource must use HTTP(S)`);
|
|
52
|
+
this.element.dataset[property] = resource.href;
|
|
53
|
+
}
|
|
38
54
|
this.element.addEventListener("preferencepanes:change", this.#change);
|
|
39
55
|
this.element.addEventListener("preferencepanes:confirm", this.#confirmation);
|
|
40
56
|
this.element.addEventListener("preferencepanes:notice", this.#notice);
|
|
@@ -60,7 +76,7 @@ export class ModuleFrame extends EventTarget {
|
|
|
60
76
|
if (this.#options.signal?.aborted) this.destroy();
|
|
61
77
|
const timer = setTimeout(() => this.#controller.abort(), 10000);
|
|
62
78
|
try {
|
|
63
|
-
const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit",
|
|
79
|
+
const response = await fetch(this.#url, { method: "GET", cache: "no-store", credentials: "omit", signal: this.#controller.signal });
|
|
64
80
|
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
|
|
65
81
|
const html = await response.text();
|
|
66
82
|
this.#controller.signal.throwIfAborted();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* 静态 HTML 的模块 iframe 容器;页面资源输入写入 iframe 数据属性。
|
|
3
|
+
* Static HTML module iframe whose resource inputs are written to iframe data attributes.
|
|
4
4
|
* confirm 事件可 preventDefault 后通过 detail.resolve/reject 完成宿主确认。
|
|
5
5
|
* Prevent default on confirm events and settle host dialogs through detail.resolve/reject.
|
|
6
6
|
* notice 事件可 preventDefault 后交由宿主显示,模块不再创建网页 Toast。
|
|
@@ -13,7 +13,7 @@ export class ModuleFrame extends EventTarget {
|
|
|
13
13
|
* 创建容器。
|
|
14
14
|
* Create a container.
|
|
15
15
|
* @param url 模块地址 / Module URL.
|
|
16
|
-
* @param options
|
|
16
|
+
* @param options 静态页面请求与资源输入 / Static page request and resource inputs.
|
|
17
17
|
*/
|
|
18
18
|
constructor(url: string | URL, options?: { signal?: AbortSignal; headers?: HeadersInit });
|
|
19
19
|
/**
|
package/src/browser/index.mjs
CHANGED
|
@@ -25,8 +25,8 @@ class ModulePage {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
* Read BoxJS JSON
|
|
28
|
+
* 读取页面声明的 BoxJS JSON 并挂载通用前端。
|
|
29
|
+
* Read the BoxJS JSON declared by the page and mount the generic frontend.
|
|
30
30
|
* @returns {Promise<void>} 启动完成 / Startup completion.
|
|
31
31
|
*/
|
|
32
32
|
async start() {
|
|
@@ -38,7 +38,23 @@ class ModulePage {
|
|
|
38
38
|
const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#window.location.pathname);
|
|
39
39
|
const module = embedded ?? match?.[1];
|
|
40
40
|
if (!module) throw new TypeError("Open a concrete module URL");
|
|
41
|
-
const
|
|
41
|
+
const frame = this.#window.frameElement?.dataset;
|
|
42
|
+
const base = frame?.preferencePanesBase ?? this.#window.location.href;
|
|
43
|
+
this.#window.document.querySelector("link[data-preference-panes-stylesheet]")?.remove();
|
|
44
|
+
const stylesheetSource = frame?.preferencePanesStylesheet ?? new URLSearchParams(this.#window.location.search).get("css")?.trim();
|
|
45
|
+
if (stylesheetSource) {
|
|
46
|
+
const stylesheet = new URL(stylesheetSource, base);
|
|
47
|
+
if (!["http:", "https:"].includes(stylesheet.protocol)) throw new TypeError("CSS resource must use HTTP(S)");
|
|
48
|
+
const link = this.#window.document.createElement("link");
|
|
49
|
+
link.dataset.preferencePanesStylesheet = "true";
|
|
50
|
+
link.rel = "stylesheet";
|
|
51
|
+
link.href = stylesheet.href;
|
|
52
|
+
this.#window.document.head.append(link);
|
|
53
|
+
}
|
|
54
|
+
const jsonSource = frame?.preferencePanesJson ?? new URLSearchParams(this.#window.location.search).get("json")?.trim();
|
|
55
|
+
const source = new URL(jsonSource || `/api/${encodeURIComponent(module)}`, base);
|
|
56
|
+
if (!["http:", "https:"].includes(source.protocol)) throw new TypeError("BoxJS resource must use HTTP(S)");
|
|
57
|
+
const response = await fetch(source, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
|
|
42
58
|
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
|
|
43
59
|
const boxjs = await response.json();
|
|
44
60
|
normalizeBoxJs(boxjs, module);
|
package/src/browser/module.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
6
6
|
<meta name="color-scheme" content="light dark">
|
|
7
|
+
<!--__PREFERENCE_PANES_JSON__-->
|
|
7
8
|
<title>Module Preferences</title>
|
|
8
9
|
<style>body { margin: 0; }</style>
|
|
9
10
|
<!--__PREFERENCE_PANES_STYLESHEET__-->
|