@nsnanocat/preference-panes 0.8.1 → 0.9.0
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 +8 -0
- package/dist/api.js +1 -1
- package/dist/module/app.mjs +142 -40
- package/dist/module/index.html +1 -1
- package/dist/module/navigation.mjs +6 -0
- package/dist/preference-panes.mjs +142 -40
- package/package.json +2 -1
- package/src/browser/ModuleFrame.mjs +6 -0
- package/src/browser/Navigation.d.mts +28 -0
- package/src/browser/components.mjs +38 -0
- package/src/browser/index.mjs +26 -2
- package/src/browser/panel.css +21 -128
- package/src/browser/panel.mjs +78 -38
- package/src/browser/vendor/b-style.min.css +7 -0
- package/src/browser/vendor/message-settings-BD3N1lqQ.css +1 -0
- package/src/browser/vendor/messageSettingsLayout-ltzQ1gMi.css +1 -0
- package/src/browser/vendor/provenance.json +31 -0
- package/src/browser/vendor/theme.min.css +1198 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nsnanocat/preference-panes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Shared settings API runtime for JavaScript proxy modules",
|
|
5
5
|
"author": "VirgilClyne <Virgil@nanocat.me>",
|
|
6
6
|
"homepage": "https://NSNanoCat.github.io/preference-panes",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"type": "module",
|
|
19
19
|
"main": "src/index.mjs",
|
|
20
20
|
"exports": {
|
|
21
|
+
"./styles/*": "./src/browser/vendor/*",
|
|
21
22
|
".": {
|
|
22
23
|
"types": "./src/index.d.ts",
|
|
23
24
|
"import": "./src/index.mjs"
|
|
@@ -14,6 +14,10 @@ export class ModuleFrame extends EventTarget {
|
|
|
14
14
|
this.#state = { ...event.detail, actions: event.detail.actions ?? [] };
|
|
15
15
|
this.dispatchEvent(new Event("change"));
|
|
16
16
|
};
|
|
17
|
+
#confirmation = event => {
|
|
18
|
+
const request = new CustomEvent("confirm", { cancelable: true, detail: event.detail });
|
|
19
|
+
if (!this.dispatchEvent(request)) event.preventDefault();
|
|
20
|
+
};
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* 建立 iframe 与请求输入;调用方挂载 element 后调用 load。
|
|
@@ -30,6 +34,7 @@ export class ModuleFrame extends EventTarget {
|
|
|
30
34
|
this.element.title = `${inputs.module} 设置`;
|
|
31
35
|
this.element.dataset.preferencePanes = JSON.stringify(inputs);
|
|
32
36
|
this.element.addEventListener("preferencepanes:change", this.#change);
|
|
37
|
+
this.element.addEventListener("preferencepanes:confirm", this.#confirmation);
|
|
33
38
|
this.#state = { title: inputs.module, module: inputs.module, busy: false, canGoBack: true, actions: [] };
|
|
34
39
|
options.signal?.addEventListener("abort", this.#abort, { once: true });
|
|
35
40
|
}
|
|
@@ -90,5 +95,6 @@ export class ModuleFrame extends EventTarget {
|
|
|
90
95
|
this.#controller.abort();
|
|
91
96
|
this.#options.signal?.removeEventListener("abort", this.#abort);
|
|
92
97
|
this.element.removeEventListener("preferencepanes:change", this.#change);
|
|
98
|
+
this.element.removeEventListener("preferencepanes:confirm", this.#confirmation);
|
|
93
99
|
}
|
|
94
100
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 原始 HTML 的模块 iframe 容器;通过元素传递请求上下文。
|
|
3
3
|
* Iframe container preserving module HTML and carrying context on the element.
|
|
4
|
+
* confirm 事件可 preventDefault 后通过 detail.resolve/reject 完成宿主确认。
|
|
5
|
+
* Prevent default on confirm events and settle host dialogs through detail.resolve/reject.
|
|
4
6
|
*/
|
|
5
7
|
export class ModuleFrame extends EventTarget {
|
|
6
8
|
/**
|
|
@@ -47,6 +49,32 @@ export class ModuleFrame extends EventTarget {
|
|
|
47
49
|
destroy(): void;
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
/**
|
|
53
|
+
* 宿主确认事件的数据。
|
|
54
|
+
* Host confirmation event detail.
|
|
55
|
+
*/
|
|
56
|
+
export interface ConfirmationRequest {
|
|
57
|
+
/**
|
|
58
|
+
* 确认内容。
|
|
59
|
+
* Confirmation message.
|
|
60
|
+
*/
|
|
61
|
+
message: string;
|
|
62
|
+
/**
|
|
63
|
+
* 返回选择。
|
|
64
|
+
* Return the user's choice.
|
|
65
|
+
* @param confirmed 是否确认 / Whether confirmed.
|
|
66
|
+
* @returns 无返回值 / No return value.
|
|
67
|
+
*/
|
|
68
|
+
resolve(confirmed: boolean): void;
|
|
69
|
+
/**
|
|
70
|
+
* 返回失败。
|
|
71
|
+
* Return a failure.
|
|
72
|
+
* @param error 错误 / Error.
|
|
73
|
+
* @returns 无返回值 / No return value.
|
|
74
|
+
*/
|
|
75
|
+
reject(error: Error): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
50
78
|
/**
|
|
51
79
|
* 菜单操作描述。
|
|
52
80
|
* Menu action descriptor.
|
|
@@ -10,10 +10,30 @@
|
|
|
10
10
|
export function element(tag, className, text) {
|
|
11
11
|
const node = document.createElement(tag);
|
|
12
12
|
node.className = className;
|
|
13
|
+
// 官方 AppSettings 1.1.2 的作用域标记与原版 CSS 一起固定版本。
|
|
14
|
+
// Pin official AppSettings 1.1.2 scope attributes together with its unmodified CSS.
|
|
15
|
+
if (/\bform-row(?:\b|__)/.test(className)) node.setAttribute("data-v-b69aa1ea", "");
|
|
16
|
+
if (/\bform-group(?:\b|__)/.test(className)) node.setAttribute("data-v-e590be47", "");
|
|
13
17
|
if (text !== undefined) node.textContent = text;
|
|
14
18
|
return node;
|
|
15
19
|
}
|
|
16
20
|
|
|
21
|
+
/**
|
|
22
|
+
* 搜索、选择和文本控件共用官方 VField 的 DOM 结构。
|
|
23
|
+
* Share the official VField DOM structure across search, select and text controls.
|
|
24
|
+
* @param {HTMLElement} control 已创建的原生控件 / Existing native control.
|
|
25
|
+
* @param {boolean} [multiline] 是否为多行输入 / Whether the control is multiline.
|
|
26
|
+
* @returns {HTMLDivElement} 字段容器 / Field container.
|
|
27
|
+
*/
|
|
28
|
+
export function fieldControl(control, multiline = false) {
|
|
29
|
+
const field = element("div", `v-field pp-editor${multiline ? " v-field--textarea" : ""}`);
|
|
30
|
+
const body = element("div", "v-field__body");
|
|
31
|
+
control.classList.add("v-field__control");
|
|
32
|
+
body.append(control);
|
|
33
|
+
field.append(body);
|
|
34
|
+
return field;
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
/**
|
|
18
38
|
* 元数据地址只允许 HTTP(S) 和相对地址。
|
|
19
39
|
* Allow only HTTP(S) and relative metadata addresses.
|
|
@@ -57,3 +77,21 @@ export function errorView(error, retry) {
|
|
|
57
77
|
view.append(element("p", "", `加载失败:${error.message}`), button);
|
|
58
78
|
return view;
|
|
59
79
|
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 请求宿主确认;独立网页使用浏览器对话框。
|
|
83
|
+
* Request confirmation from the host, using the browser dialog for standalone pages.
|
|
84
|
+
* @param {Window} host 模块窗口 / Module window.
|
|
85
|
+
* @param {string} message 确认内容 / Confirmation message.
|
|
86
|
+
* @returns {Promise<boolean>} 用户是否确认 / Whether the user confirmed.
|
|
87
|
+
*/
|
|
88
|
+
export function requestConfirmation(host, message) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const frame = host.frameElement;
|
|
91
|
+
if (frame) {
|
|
92
|
+
const event = new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:confirm", { cancelable: true, detail: { message, resolve, reject } });
|
|
93
|
+
if (!frame.dispatchEvent(event)) return;
|
|
94
|
+
}
|
|
95
|
+
resolve(host.confirm(message));
|
|
96
|
+
});
|
|
97
|
+
}
|
package/src/browser/index.mjs
CHANGED
|
@@ -30,8 +30,28 @@ export function mount(boxjs, css = "") {
|
|
|
30
30
|
document.head.append(base, custom);
|
|
31
31
|
const previousTitle = document.title;
|
|
32
32
|
const previousTheme = document.documentElement.dataset.theme;
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const previousDark = document.documentElement.classList.contains("bili_dark");
|
|
34
|
+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
35
|
+
const previousKeyboard = document.documentElement.style.getPropertyValue("--pp-keyboard-height");
|
|
36
|
+
const host = window.frameElement?.ownerDocument.documentElement;
|
|
37
|
+
/**
|
|
38
|
+
* 跟随嵌入宿主的通用环境状态,不识别业务 App 或解析其 UA。
|
|
39
|
+
* Follow generic host appearance without detecting a business app or parsing its user agent.
|
|
40
|
+
* @returns {void} 已同步主题与键盘避让 / Theme and keyboard clearance synchronized.
|
|
41
|
+
*/
|
|
42
|
+
const syncAppearance = () => {
|
|
43
|
+
const theme = host?.dataset.theme ?? previousTheme ?? (systemTheme.matches ? "dark" : "light");
|
|
44
|
+
document.documentElement.dataset.theme = theme;
|
|
45
|
+
document.documentElement.classList.toggle("bili_dark", theme === "dark");
|
|
46
|
+
if (host) document.documentElement.style.setProperty("--pp-keyboard-height", host.style.getPropertyValue("--pp-keyboard-height"));
|
|
47
|
+
};
|
|
48
|
+
let observer;
|
|
49
|
+
syncAppearance();
|
|
50
|
+
systemTheme.addEventListener("change", syncAppearance);
|
|
51
|
+
if (host) {
|
|
52
|
+
observer = new MutationObserver(syncAppearance);
|
|
53
|
+
observer.observe(host, { attributes: true, attributeFilter: ["data-theme", "style"] });
|
|
54
|
+
}
|
|
35
55
|
document.title = metadata.name ?? catalog.module.module;
|
|
36
56
|
let panel;
|
|
37
57
|
const view = {
|
|
@@ -41,6 +61,9 @@ export function mount(boxjs, css = "") {
|
|
|
41
61
|
* @returns {void} 无返回值 / No return value.
|
|
42
62
|
*/
|
|
43
63
|
destroy() {
|
|
64
|
+
observer?.disconnect();
|
|
65
|
+
systemTheme.removeEventListener("change", syncAppearance);
|
|
66
|
+
document.documentElement.classList.toggle("bili_dark", previousDark);
|
|
44
67
|
panel?.destroy();
|
|
45
68
|
base.remove();
|
|
46
69
|
custom.remove();
|
|
@@ -49,6 +72,7 @@ export function mount(boxjs, css = "") {
|
|
|
49
72
|
document.title = previousTitle;
|
|
50
73
|
if (previousTheme === undefined) delete document.documentElement.dataset.theme;
|
|
51
74
|
else document.documentElement.dataset.theme = previousTheme;
|
|
75
|
+
document.documentElement.style.setProperty("--pp-keyboard-height", previousKeyboard);
|
|
52
76
|
},
|
|
53
77
|
};
|
|
54
78
|
try {
|
package/src/browser/panel.css
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* 分组列表沿用 Bilibili 设置页的行结构,样式限定在面板内。
|
|
2
2
|
* Grouped rows follow the Bilibili settings layout, scoped to the panel. */
|
|
3
3
|
.pp-panel {
|
|
4
|
-
--pp-text:
|
|
5
|
-
--pp-background:
|
|
6
|
-
--pp-surface:
|
|
7
|
-
--pp-border:
|
|
8
|
-
--pp-muted:
|
|
9
|
-
--pp-accent:
|
|
4
|
+
--pp-text: var(--text1);
|
|
5
|
+
--pp-background: var(--bg2);
|
|
6
|
+
--pp-surface: var(--bg1);
|
|
7
|
+
--pp-border: var(--line_regular);
|
|
8
|
+
--pp-muted: var(--text3);
|
|
9
|
+
--pp-accent: var(--brand_pink);
|
|
10
10
|
font:
|
|
11
11
|
15px / 1.5 -apple-system,
|
|
12
12
|
BlinkMacSystemFont,
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
width: 44px;
|
|
68
68
|
flex: none;
|
|
69
69
|
}
|
|
70
|
-
.pp-panel button {
|
|
70
|
+
.pp-panel button:not(.v-toggle) {
|
|
71
71
|
font: inherit;
|
|
72
72
|
cursor: pointer;
|
|
73
73
|
border: 0;
|
|
@@ -111,60 +111,10 @@
|
|
|
111
111
|
position: absolute;
|
|
112
112
|
inset: 0;
|
|
113
113
|
overflow: auto;
|
|
114
|
-
padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom));
|
|
114
|
+
padding: 12px max(16px, calc((100% - 688px) / 2)) calc(28px + env(safe-area-inset-bottom) + var(--pp-keyboard-height, 0px));
|
|
115
|
+
scroll-padding-bottom: var(--pp-keyboard-height, 0px);
|
|
115
116
|
background: var(--pp-background);
|
|
116
117
|
}
|
|
117
|
-
.pp-panel .form-group {
|
|
118
|
-
margin: 0 0 12px;
|
|
119
|
-
}
|
|
120
|
-
.pp-panel .form-group__title {
|
|
121
|
-
font-size: 12px;
|
|
122
|
-
line-height: 17px;
|
|
123
|
-
font-weight: 400;
|
|
124
|
-
color: var(--pp-muted);
|
|
125
|
-
padding-left: 12px;
|
|
126
|
-
margin: 12px 0 6px;
|
|
127
|
-
}
|
|
128
|
-
.pp-panel .form-group__row {
|
|
129
|
-
border-radius: 8px;
|
|
130
|
-
overflow: hidden;
|
|
131
|
-
background: var(--pp-surface);
|
|
132
|
-
}
|
|
133
|
-
.pp-panel .form-row {
|
|
134
|
-
position: relative;
|
|
135
|
-
display: flex;
|
|
136
|
-
align-items: center;
|
|
137
|
-
width: 100%;
|
|
138
|
-
min-height: 46px;
|
|
139
|
-
padding: 12px;
|
|
140
|
-
border: 0;
|
|
141
|
-
border-bottom: 1px solid var(--pp-border);
|
|
142
|
-
background: var(--pp-surface);
|
|
143
|
-
gap: 12px;
|
|
144
|
-
}
|
|
145
|
-
.pp-panel .form-row:last-child {
|
|
146
|
-
border-bottom: 0;
|
|
147
|
-
}
|
|
148
|
-
.pp-panel .form-row__text {
|
|
149
|
-
flex: 1;
|
|
150
|
-
min-width: 0;
|
|
151
|
-
margin: 0;
|
|
152
|
-
display: flex;
|
|
153
|
-
flex-direction: column;
|
|
154
|
-
}
|
|
155
|
-
.pp-panel .form-row__title {
|
|
156
|
-
font-size: 15px;
|
|
157
|
-
line-height: 22px;
|
|
158
|
-
color: var(--pp-text);
|
|
159
|
-
text-align: left;
|
|
160
|
-
}
|
|
161
|
-
.pp-panel .form-row__subtitle {
|
|
162
|
-
font-size: 12px;
|
|
163
|
-
line-height: 18px;
|
|
164
|
-
color: var(--pp-muted);
|
|
165
|
-
overflow-wrap: anywhere;
|
|
166
|
-
margin-top: 2px;
|
|
167
|
-
}
|
|
168
118
|
.pp-choice-link {
|
|
169
119
|
display: flex;
|
|
170
120
|
align-items: center;
|
|
@@ -192,60 +142,29 @@
|
|
|
192
142
|
font-size: 22px;
|
|
193
143
|
flex: none;
|
|
194
144
|
}
|
|
195
|
-
.pp-
|
|
196
|
-
|
|
197
|
-
color: var(--pp-text);
|
|
198
|
-
background: var(--pp-surface);
|
|
199
|
-
border: 1px solid var(--pp-border);
|
|
200
|
-
border-radius: 6px;
|
|
201
|
-
padding: 8px;
|
|
202
|
-
min-width: 0;
|
|
203
|
-
max-width: 45%;
|
|
145
|
+
.pp-editor {
|
|
146
|
+
flex: none;
|
|
204
147
|
width: 45%;
|
|
148
|
+
min-width: 0;
|
|
205
149
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
150
|
+
.pp-search {
|
|
151
|
+
width: 100%;
|
|
152
|
+
margin-bottom: 12px;
|
|
209
153
|
}
|
|
210
154
|
.pp-panel .pp-multiline {
|
|
211
155
|
display: block;
|
|
212
156
|
}
|
|
213
|
-
.pp-multiline .pp-
|
|
214
|
-
max-width: 100%;
|
|
157
|
+
.pp-multiline .pp-editor {
|
|
215
158
|
width: 100%;
|
|
216
159
|
margin-top: 10px;
|
|
217
160
|
}
|
|
218
|
-
.pp-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
flex: none;
|
|
223
|
-
width: 32px;
|
|
224
|
-
height: 20px;
|
|
225
|
-
max-width: none;
|
|
161
|
+
.pp-panel [hidden] {
|
|
162
|
+
display: none !important;
|
|
163
|
+
}
|
|
164
|
+
.pp-panel .v-toggle {
|
|
226
165
|
border: 0;
|
|
227
|
-
border-radius: 15px;
|
|
228
166
|
padding: 0;
|
|
229
|
-
|
|
230
|
-
cursor: pointer;
|
|
231
|
-
transition: background 0.2s;
|
|
232
|
-
}
|
|
233
|
-
.pp-switch::before {
|
|
234
|
-
content: "";
|
|
235
|
-
position: absolute;
|
|
236
|
-
top: 3px;
|
|
237
|
-
left: 3px;
|
|
238
|
-
width: 14px;
|
|
239
|
-
height: 14px;
|
|
240
|
-
border-radius: 50%;
|
|
241
|
-
background: white;
|
|
242
|
-
transition: transform 0.2s;
|
|
243
|
-
}
|
|
244
|
-
.pp-switch:checked {
|
|
245
|
-
background: var(--pp-accent);
|
|
246
|
-
}
|
|
247
|
-
.pp-switch:checked::before {
|
|
248
|
-
transform: translateX(12px);
|
|
167
|
+
flex: none;
|
|
249
168
|
}
|
|
250
169
|
.pp-choice {
|
|
251
170
|
justify-content: space-between;
|
|
@@ -315,29 +234,3 @@ select.pp-input {
|
|
|
315
234
|
outline: 2px solid var(--pp-accent);
|
|
316
235
|
outline-offset: -2px;
|
|
317
236
|
}
|
|
318
|
-
@media (prefers-color-scheme: dark) {
|
|
319
|
-
.pp-panel {
|
|
320
|
-
--pp-text: #e3e5e7;
|
|
321
|
-
--pp-background: #17181a;
|
|
322
|
-
--pp-surface: #232427;
|
|
323
|
-
--pp-border: #343538;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
:root[data-theme="dark"] .pp-panel {
|
|
327
|
-
--pp-text: #e3e5e7;
|
|
328
|
-
--pp-background: #17181a;
|
|
329
|
-
--pp-surface: #232427;
|
|
330
|
-
--pp-border: #343538;
|
|
331
|
-
}
|
|
332
|
-
:root[data-theme="light"] .pp-panel {
|
|
333
|
-
--pp-text: #18191c;
|
|
334
|
-
--pp-background: #f6f7f8;
|
|
335
|
-
--pp-surface: #fff;
|
|
336
|
-
--pp-border: #e3e5e7;
|
|
337
|
-
}
|
|
338
|
-
@media (prefers-reduced-motion: reduce) {
|
|
339
|
-
.pp-panel .pp-switch,
|
|
340
|
-
.pp-panel .pp-switch::before {
|
|
341
|
-
transition: none;
|
|
342
|
-
}
|
|
343
|
-
}
|
package/src/browser/panel.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ActionMenu } from "./ActionMenu.mjs";
|
|
2
2
|
import { createPreferencesClient } from "./client.mjs";
|
|
3
|
-
import { errorView, icon, element as node, resourceURL } from "./components.mjs";
|
|
3
|
+
import { errorView, fieldControl, icon, element as node, requestConfirmation, resourceURL } from "./components.mjs";
|
|
4
4
|
import { Navigation } from "./Navigation.mjs";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -27,7 +27,7 @@ export function mountPanel(root, catalog) {
|
|
|
27
27
|
{ id: "clearCaches", label: "清空缓存", destructive: true },
|
|
28
28
|
{ id: "reset", label: "重置模块", destructive: true },
|
|
29
29
|
];
|
|
30
|
-
const menu = new ActionMenu(id =>
|
|
30
|
+
const menu = new ActionMenu(id => runAction(id));
|
|
31
31
|
const trailing = node("span", "pp-nav-spacer");
|
|
32
32
|
trailing.append(menu.element);
|
|
33
33
|
const brand = node("div", "pp-brand");
|
|
@@ -57,7 +57,7 @@ export function mountPanel(root, catalog) {
|
|
|
57
57
|
);
|
|
58
58
|
};
|
|
59
59
|
const onAction = event => {
|
|
60
|
-
if (!saving && handlers.has(event.detail))
|
|
60
|
+
if (!saving && handlers.has(event.detail)) runAction(event.detail);
|
|
61
61
|
};
|
|
62
62
|
window.frameElement?.addEventListener("preferencepanes:action", onAction);
|
|
63
63
|
let timer,
|
|
@@ -99,6 +99,19 @@ export function mountPanel(root, catalog) {
|
|
|
99
99
|
}, 2400);
|
|
100
100
|
};
|
|
101
101
|
const client = createPreferencesClient({ catalog, notify });
|
|
102
|
+
/**
|
|
103
|
+
* 两种菜单入口共用异步错误处理,包含宿主确认框错误。
|
|
104
|
+
* Share async error handling between both menus, including host-dialog errors.
|
|
105
|
+
* @param {string} id 操作标识 / Action identifier.
|
|
106
|
+
* @returns {Promise<void>} 操作已处理 / Action handled.
|
|
107
|
+
*/
|
|
108
|
+
async function runAction(id) {
|
|
109
|
+
try {
|
|
110
|
+
await handlers.get(id)();
|
|
111
|
+
} catch (error) {
|
|
112
|
+
notify({ kind: "error", message: error.message });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
102
115
|
/**
|
|
103
116
|
* 打开模块并忽略已过期的异步结果。
|
|
104
117
|
* Open a module and ignore stale asynchronous results.
|
|
@@ -130,6 +143,14 @@ export function mountPanel(root, catalog) {
|
|
|
130
143
|
const { definition, values } = client.snapshot(active);
|
|
131
144
|
heading.textContent = definition.metadata?.name || active;
|
|
132
145
|
const view = node("section", "pp-fields");
|
|
146
|
+
const search = node("input", "");
|
|
147
|
+
search.type = "search";
|
|
148
|
+
search.placeholder = "搜索设置项";
|
|
149
|
+
search.setAttribute("aria-label", "搜索设置");
|
|
150
|
+
const searchField = fieldControl(search);
|
|
151
|
+
searchField.classList.add("pp-search");
|
|
152
|
+
view.append(searchField);
|
|
153
|
+
const searchRows = [];
|
|
133
154
|
/**
|
|
134
155
|
* 挂载后执行的多行高度更新
|
|
135
156
|
* Textarea sizing callbacks run after mounting.
|
|
@@ -204,7 +225,7 @@ export function mountPanel(root, catalog) {
|
|
|
204
225
|
const match = /^\[([^\]]+)\]\s*(.*)$/.exec(field.name);
|
|
205
226
|
const group = match?.[1] ?? "通用";
|
|
206
227
|
if (!groups.has(group)) {
|
|
207
|
-
const section = node("section", "form-group");
|
|
228
|
+
const section = node("section", "form-group form-group--has-title");
|
|
208
229
|
const rows = node("div", "form-group__row");
|
|
209
230
|
section.append(node("h2", "form-group__title", group), rows);
|
|
210
231
|
groups.set(group, rows);
|
|
@@ -232,7 +253,7 @@ export function mountPanel(root, catalog) {
|
|
|
232
253
|
let eventName = "change";
|
|
233
254
|
switch (true) {
|
|
234
255
|
case Boolean(field.options) && field.type !== "array": {
|
|
235
|
-
const select = node("select", "
|
|
256
|
+
const select = node("select", "");
|
|
236
257
|
select.setAttribute("aria-label", field.name);
|
|
237
258
|
field.options.forEach((option, index) => {
|
|
238
259
|
const item = node("option", "", option.label);
|
|
@@ -242,7 +263,7 @@ export function mountPanel(root, catalog) {
|
|
|
242
263
|
write = value => {
|
|
243
264
|
select.selectedIndex = field.options.findIndex(option => option.key === value);
|
|
244
265
|
};
|
|
245
|
-
row.append(select);
|
|
266
|
+
row.append(fieldControl(select));
|
|
246
267
|
read = () => field.options[select.selectedIndex]?.key;
|
|
247
268
|
break;
|
|
248
269
|
}
|
|
@@ -288,9 +309,27 @@ export function mountPanel(root, catalog) {
|
|
|
288
309
|
};
|
|
289
310
|
break;
|
|
290
311
|
}
|
|
312
|
+
case field.type === "boolean": {
|
|
313
|
+
const toggle = node("button", "v-toggle v-toggle--small form-row__toggle");
|
|
314
|
+
toggle.type = "button";
|
|
315
|
+
toggle.setAttribute("role", "switch");
|
|
316
|
+
toggle.setAttribute("aria-label", field.name);
|
|
317
|
+
toggle.append(node("span", "v-toggle__circle"));
|
|
318
|
+
write = value => {
|
|
319
|
+
toggle.setAttribute("aria-checked", String(value === true));
|
|
320
|
+
toggle.classList.toggle("v-toggle--closed", value !== true);
|
|
321
|
+
};
|
|
322
|
+
read = () => toggle.getAttribute("aria-checked") === "true";
|
|
323
|
+
toggle.onclick = () => {
|
|
324
|
+
write(!read());
|
|
325
|
+
toggle.dispatchEvent(new window.Event("change", { bubbles: true }));
|
|
326
|
+
};
|
|
327
|
+
row.append(toggle);
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
291
330
|
default: {
|
|
292
331
|
const multiline = field.control === "textarea" || field.type === "array";
|
|
293
|
-
const input = node(multiline ? "textarea" : "input", "
|
|
332
|
+
const input = node(multiline ? "textarea" : "input", "");
|
|
294
333
|
if (multiline) row.classList.add("pp-multiline");
|
|
295
334
|
input.setAttribute("aria-label", field.name);
|
|
296
335
|
if (field.placeholder) input.placeholder = field.placeholder;
|
|
@@ -312,33 +351,23 @@ export function mountPanel(root, catalog) {
|
|
|
312
351
|
input.addEventListener("input", grow);
|
|
313
352
|
growingInputs.push(grow);
|
|
314
353
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
input.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
case "array":
|
|
333
|
-
return JSON.parse(input.value);
|
|
334
|
-
case "number":
|
|
335
|
-
return input.value === "" ? Number.NaN : Number(input.value);
|
|
336
|
-
default:
|
|
337
|
-
return input.value;
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
row.append(input);
|
|
354
|
+
eventName = "input";
|
|
355
|
+
if (!multiline) input.type = field.type === "number" ? "number" : "text";
|
|
356
|
+
write = value => {
|
|
357
|
+
input.value = field.type === "array" ? JSON.stringify(value ?? []) : (value ?? "");
|
|
358
|
+
grow();
|
|
359
|
+
};
|
|
360
|
+
read = () => {
|
|
361
|
+
switch (field.type) {
|
|
362
|
+
case "array":
|
|
363
|
+
return JSON.parse(input.value);
|
|
364
|
+
case "number":
|
|
365
|
+
return input.value === "" ? Number.NaN : Number(input.value);
|
|
366
|
+
default:
|
|
367
|
+
return input.value;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
row.append(fieldControl(input, multiline));
|
|
342
371
|
break;
|
|
343
372
|
}
|
|
344
373
|
}
|
|
@@ -368,7 +397,18 @@ export function mountPanel(root, catalog) {
|
|
|
368
397
|
});
|
|
369
398
|
if (eventName === "input") inputContainer.addEventListener("compositionend", event => event.target.dispatchEvent(new window.Event("input", { bubbles: true })));
|
|
370
399
|
groups.get(group).append(row);
|
|
400
|
+
searchRows.push({ row, text: [field.name, field.key, field.description, ...(field.options ?? []).map(option => option.label)].join(" ").toLocaleLowerCase() });
|
|
371
401
|
}
|
|
402
|
+
const empty = node("p", "pp-description", "没有匹配的设置项");
|
|
403
|
+
empty.hidden = true;
|
|
404
|
+
empty.setAttribute("role", "status");
|
|
405
|
+
view.append(empty);
|
|
406
|
+
search.oninput = () => {
|
|
407
|
+
const words = search.value.trim().toLocaleLowerCase().split(/\s+/);
|
|
408
|
+
for (const { row, text } of searchRows) row.hidden = !words.every(word => text.includes(word));
|
|
409
|
+
for (const rows of groups.values()) rows.parentElement.hidden = [...rows.children].every(row => row.hidden);
|
|
410
|
+
empty.hidden = searchRows.some(({ row }) => !row.hidden);
|
|
411
|
+
};
|
|
372
412
|
const cachePage = node("section", "pp-cache-page");
|
|
373
413
|
const output = node("pre", "pp-cache");
|
|
374
414
|
output.textContent = "暂无缓存";
|
|
@@ -393,9 +433,9 @@ export function mountPanel(root, catalog) {
|
|
|
393
433
|
},
|
|
394
434
|
);
|
|
395
435
|
});
|
|
396
|
-
handlers.set("clearCaches", () => {
|
|
436
|
+
handlers.set("clearCaches", async () => {
|
|
397
437
|
if (saving) return;
|
|
398
|
-
if (!window
|
|
438
|
+
if (!(await requestConfirmation(window, `清空 ${active} 的全部 Caches?`)) || destroyed || saving) return;
|
|
399
439
|
return perform(
|
|
400
440
|
() => client.clearCaches(active),
|
|
401
441
|
() => {
|
|
@@ -403,9 +443,9 @@ export function mountPanel(root, catalog) {
|
|
|
403
443
|
},
|
|
404
444
|
);
|
|
405
445
|
});
|
|
406
|
-
handlers.set("reset", () => {
|
|
446
|
+
handlers.set("reset", async () => {
|
|
407
447
|
if (saving) return;
|
|
408
|
-
if (!window
|
|
448
|
+
if (!(await requestConfirmation(window, `重置 ${active}?这将删除该模块的 Settings、Caches 和其它持久化数据。`)) || destroyed || saving) return;
|
|
409
449
|
return perform(() => client.reset(active), controls);
|
|
410
450
|
});
|
|
411
451
|
navigation?.destroy();
|