@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
package/src/browser/home.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ActionMenu,
|
|
1
|
+
import { ActionMenu, ModuleStatus } from "./Navigation.mjs";
|
|
2
|
+
import { browserHost, PageController } from "./page.mjs";
|
|
2
3
|
import { installDefaultStyles } from "./styles.mjs";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -10,6 +11,11 @@ import { installDefaultStyles } from "./styles.mjs";
|
|
|
10
11
|
function validateHome(config) {
|
|
11
12
|
if (!config || typeof config.title !== "string" || !Array.isArray(config.sections)) throw new Error("首页 JSON 缺少 title 或 sections");
|
|
12
13
|
if (config.logo !== undefined && typeof config.logo !== "string") throw new Error("首页 logo 必须是图片地址");
|
|
14
|
+
if (config.logoText !== undefined && typeof config.logoText !== "string") throw new Error("首页 logoText 必须是文字");
|
|
15
|
+
if (config.logoHref !== undefined) {
|
|
16
|
+
if (typeof config.logoHref !== "string") throw new Error("首页 logoHref 必须是链接地址");
|
|
17
|
+
if (["javascript:", "data:", "vbscript:"].includes(new URL(config.logoHref, "https://preference-panes.invalid/").protocol)) throw new Error("首页 logoHref 的链接无效");
|
|
18
|
+
}
|
|
13
19
|
if (config.footer !== undefined && (!Array.isArray(config.footer) || config.footer.some(text => typeof text !== "string"))) throw new Error("首页 footer 必须是文字数组");
|
|
14
20
|
const modules = new Set();
|
|
15
21
|
for (const section of config.sections) {
|
|
@@ -21,11 +27,11 @@ function validateHome(config) {
|
|
|
21
27
|
if (item.target !== undefined && (item.module !== undefined || !["internal", "_self", "_blank"].includes(item.target))) throw new Error(`入口 ${item.name} 的 target 只允许链接使用 internal、_self 或 _blank`);
|
|
22
28
|
if ((typeof item.module === "string") === (typeof item.href === "string") || (item.module !== undefined && typeof item.module !== "string") || (item.href !== undefined && typeof item.href !== "string")) throw new Error(`入口 ${item.name} 必须选择 module 或 href`);
|
|
23
29
|
if (item.module !== undefined) {
|
|
24
|
-
if (!/^[A-Za-z0-9_-]+$/.test(item.module) ||
|
|
30
|
+
if (!/^[A-Za-z0-9_-]+$/.test(item.module) || modules.has(item.module)) throw new Error(`入口 ${item.name} 的模块名称无效或重复`);
|
|
25
31
|
modules.add(item.module);
|
|
26
32
|
} else {
|
|
27
33
|
const protocol = new URL(item.href, "https://preference-panes.invalid/").protocol;
|
|
28
|
-
if (
|
|
34
|
+
if (["javascript:", "data:", "vbscript:"].includes(protocol) || (item.target === "internal" && !["http:", "https:"].includes(protocol))) throw new Error(`入口 ${item.name} 的链接无效`);
|
|
29
35
|
}
|
|
30
36
|
}
|
|
31
37
|
}
|
|
@@ -42,13 +48,6 @@ function validateHome(config) {
|
|
|
42
48
|
function renderHome(config, url, home) {
|
|
43
49
|
const document = home.ownerDocument;
|
|
44
50
|
home.replaceChildren();
|
|
45
|
-
if (config.logo) {
|
|
46
|
-
const logo = document.createElement("img");
|
|
47
|
-
logo.className = "pp-home-logo";
|
|
48
|
-
logo.src = new URL(config.logo, url).href;
|
|
49
|
-
logo.alt = config.title;
|
|
50
|
-
home.append(logo);
|
|
51
|
-
}
|
|
52
51
|
for (const section of config.sections) {
|
|
53
52
|
const group = document.createElement("section");
|
|
54
53
|
group.className = "pp-group";
|
|
@@ -65,7 +64,6 @@ function renderHome(config, url, home) {
|
|
|
65
64
|
entry.type = "button";
|
|
66
65
|
entry.disabled = true;
|
|
67
66
|
entry.dataset.module = item.module;
|
|
68
|
-
if (item.pageModule) entry.dataset.preferencePanesModule = item.pageModule;
|
|
69
67
|
} else {
|
|
70
68
|
entry.href = new URL(item.href, url).href;
|
|
71
69
|
if (item.target !== undefined) {
|
|
@@ -132,90 +130,6 @@ function renderHome(config, url, home) {
|
|
|
132
130
|
home.append(footer);
|
|
133
131
|
}
|
|
134
132
|
|
|
135
|
-
/**
|
|
136
|
-
* 浏览器宿主提供网页导航、通知、主题和可视区域避让。
|
|
137
|
-
* The browser host provides web navigation, notices, theme and viewport clearance.
|
|
138
|
-
* @param {import("./home.mjs").BridgeOptions} options 菜单和返回动作 / Menu and back action.
|
|
139
|
-
* @returns {import("./home.mjs").Bridge} 浏览器宿主 / Browser host.
|
|
140
|
-
*/
|
|
141
|
-
function browserHost({ menu, back }) {
|
|
142
|
-
const root = document.documentElement;
|
|
143
|
-
const header = document.createElement("header");
|
|
144
|
-
header.className = "pp-host-header";
|
|
145
|
-
const button = document.createElement("button");
|
|
146
|
-
button.className = "pp-host-back";
|
|
147
|
-
button.type = "button";
|
|
148
|
-
button.textContent = "‹";
|
|
149
|
-
button.setAttribute("aria-label", "返回");
|
|
150
|
-
button.onclick = back;
|
|
151
|
-
const title = document.createElement("span");
|
|
152
|
-
title.className = "pp-host-title";
|
|
153
|
-
header.append(button, title, menu.element);
|
|
154
|
-
document.body.prepend(header);
|
|
155
|
-
const toast = document.createElement("div");
|
|
156
|
-
toast.className = "pp-host-toast";
|
|
157
|
-
toast.setAttribute("role", "status");
|
|
158
|
-
toast.hidden = true;
|
|
159
|
-
document.body.append(toast);
|
|
160
|
-
let timer;
|
|
161
|
-
const theme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
162
|
-
const applyTheme = () => {
|
|
163
|
-
root.dataset.theme = theme.matches ? "dark" : "light";
|
|
164
|
-
};
|
|
165
|
-
const viewport = window.visualViewport;
|
|
166
|
-
const resize = () => {
|
|
167
|
-
root.style.setProperty("--pp-host-height", `${header.getBoundingClientRect().height}px`);
|
|
168
|
-
const clearance = viewport && viewport.scale === 1 ? Math.max(0, window.innerHeight - viewport.height - viewport.offsetTop) : 0;
|
|
169
|
-
root.style.setProperty("--pp-keyboard-height", `${clearance}px`);
|
|
170
|
-
};
|
|
171
|
-
theme.addEventListener("change", applyTheme);
|
|
172
|
-
window.addEventListener("resize", resize);
|
|
173
|
-
window.addEventListener("pageshow", resize);
|
|
174
|
-
viewport?.addEventListener("resize", resize);
|
|
175
|
-
viewport?.addEventListener("scroll", resize);
|
|
176
|
-
applyTheme();
|
|
177
|
-
resize();
|
|
178
|
-
return {
|
|
179
|
-
update(state) {
|
|
180
|
-
title.textContent = state.title;
|
|
181
|
-
button.hidden = !state.canGoBack;
|
|
182
|
-
menu.element.hidden = !state.actions.length;
|
|
183
|
-
},
|
|
184
|
-
confirm: message => window.confirm(message),
|
|
185
|
-
notice(detail) {
|
|
186
|
-
clearTimeout(timer);
|
|
187
|
-
toast.textContent = detail.message;
|
|
188
|
-
toast.hidden = false;
|
|
189
|
-
timer = setTimeout(() => {
|
|
190
|
-
toast.hidden = true;
|
|
191
|
-
}, 2400);
|
|
192
|
-
},
|
|
193
|
-
openURL(url, target) {
|
|
194
|
-
switch (target) {
|
|
195
|
-
case "_blank":
|
|
196
|
-
window.open(url, "_blank", "noopener,noreferrer");
|
|
197
|
-
break;
|
|
198
|
-
default:
|
|
199
|
-
window.location.assign(url);
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
destroy() {
|
|
204
|
-
clearTimeout(timer);
|
|
205
|
-
theme.removeEventListener("change", applyTheme);
|
|
206
|
-
window.removeEventListener("resize", resize);
|
|
207
|
-
window.removeEventListener("pageshow", resize);
|
|
208
|
-
viewport?.removeEventListener("resize", resize);
|
|
209
|
-
viewport?.removeEventListener("scroll", resize);
|
|
210
|
-
root.style.removeProperty("--pp-host-height");
|
|
211
|
-
root.style.removeProperty("--pp-keyboard-height");
|
|
212
|
-
delete root.dataset.theme;
|
|
213
|
-
header.remove();
|
|
214
|
-
toast.remove();
|
|
215
|
-
},
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
133
|
/**
|
|
220
134
|
* 读取固定相对路径的首页 JSON 与可选 CSS、Bridge,初始化后共用模块探测与导航。
|
|
221
135
|
* Load homepage JSON and optional CSS and Bridge at fixed relative paths, then share module probes and navigation.
|
|
@@ -225,8 +139,7 @@ export async function startHome() {
|
|
|
225
139
|
let host;
|
|
226
140
|
let menu;
|
|
227
141
|
let navigation;
|
|
228
|
-
let
|
|
229
|
-
let linkTitle;
|
|
142
|
+
let page;
|
|
230
143
|
let statuses = [];
|
|
231
144
|
let destroyed = false;
|
|
232
145
|
let defaults;
|
|
@@ -237,9 +150,11 @@ export async function startHome() {
|
|
|
237
150
|
destroyed = true;
|
|
238
151
|
controller.abort();
|
|
239
152
|
for (const { status } of statuses) status.destroy();
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
153
|
+
if (page) page.destroy();
|
|
154
|
+
else {
|
|
155
|
+
host?.destroy();
|
|
156
|
+
menu?.destroy();
|
|
157
|
+
}
|
|
243
158
|
stylesheet?.remove();
|
|
244
159
|
if (defaults?.owned) defaults.element.remove();
|
|
245
160
|
window.removeEventListener("pagehide", pagehide);
|
|
@@ -301,8 +216,8 @@ export async function startHome() {
|
|
|
301
216
|
}
|
|
302
217
|
if (destroyed) return destroy;
|
|
303
218
|
document.title = config.title;
|
|
304
|
-
menu = new ActionMenu(id =>
|
|
305
|
-
host = await factory({ menu, back: () =>
|
|
219
|
+
menu = new ActionMenu(id => page.perform(id));
|
|
220
|
+
host = await factory({ menu, back: () => navigation.back() });
|
|
306
221
|
if (destroyed) {
|
|
307
222
|
host.destroy();
|
|
308
223
|
menu.destroy();
|
|
@@ -310,82 +225,20 @@ export async function startHome() {
|
|
|
310
225
|
}
|
|
311
226
|
const container = document.querySelector("[data-preference-panes-pages]");
|
|
312
227
|
const home = document.querySelector("[data-preference-panes-home]");
|
|
313
|
-
const template = document.querySelector("template[data-preference-panes-module]");
|
|
314
228
|
renderHome(config, jsonURL, home);
|
|
315
229
|
const buttons = [...home.querySelectorAll("[data-module]")];
|
|
316
230
|
const links = [...home.querySelectorAll("a[href]")];
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
navigation = new Navigation(container, home, (module, signal) => {
|
|
327
|
-
frame = undefined;
|
|
328
|
-
const link = links.find((link, index) => link.dataset.target === "internal" && module === `link:${index}`);
|
|
329
|
-
if (link) {
|
|
330
|
-
const page = document.createElement("section");
|
|
331
|
-
page.dataset.preferencePanesLinkPage = "";
|
|
332
|
-
const iframe = document.createElement("iframe");
|
|
333
|
-
linkTitle = link.querySelector(".pp-home-name").textContent;
|
|
334
|
-
iframe.title = linkTitle;
|
|
335
|
-
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox");
|
|
336
|
-
iframe.src = link.href;
|
|
337
|
-
page.append(iframe);
|
|
338
|
-
signal.addEventListener("abort", () => iframe.remove(), { once: true });
|
|
339
|
-
return page;
|
|
340
|
-
}
|
|
341
|
-
const button = buttons.find(button => button.dataset.module === module);
|
|
342
|
-
if (!button) return;
|
|
343
|
-
const page = template.content.firstElementChild.cloneNode(true);
|
|
344
|
-
page.dataset.preferencePanesModulePage = "";
|
|
345
|
-
const message = page.querySelector("[data-module-message]");
|
|
346
|
-
const moduleName = encodeURIComponent(button.dataset.preferencePanesModule ?? module);
|
|
347
|
-
const current = new ModuleFrame(`/settings/${moduleName}`, {
|
|
348
|
-
signal,
|
|
349
|
-
headers: { "X-PreferencePanes-JSON": `/api/${encodeURIComponent(module)}`, ...(stylesheetURL ? { "X-PreferencePanes-CSS": stylesheetURL } : {}) },
|
|
350
|
-
});
|
|
351
|
-
frame = current;
|
|
352
|
-
current.addEventListener("confirm", event => {
|
|
353
|
-
event.preventDefault();
|
|
354
|
-
Promise.resolve()
|
|
355
|
-
.then(() => host.confirm(event.detail.message))
|
|
356
|
-
.then(
|
|
357
|
-
value => {
|
|
358
|
-
if (!signal.aborted) event.detail.resolve(value);
|
|
359
|
-
},
|
|
360
|
-
error => {
|
|
361
|
-
if (!signal.aborted) event.detail.reject(error);
|
|
362
|
-
},
|
|
363
|
-
);
|
|
364
|
-
});
|
|
365
|
-
current.addEventListener("notice", event => {
|
|
366
|
-
event.preventDefault();
|
|
367
|
-
host.notice(event.detail);
|
|
368
|
-
});
|
|
369
|
-
current.addEventListener("open-url", event => {
|
|
370
|
-
event.preventDefault();
|
|
371
|
-
Promise.resolve()
|
|
372
|
-
.then(() => host.openURL(event.detail.url))
|
|
373
|
-
.catch(error => {
|
|
374
|
-
if (!signal.aborted) host.notice({ kind: "error", message: error.message });
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
current.addEventListener("change", () => {
|
|
378
|
-
if (!signal.aborted) update();
|
|
379
|
-
});
|
|
380
|
-
current.element.onload = () => {
|
|
381
|
-
if (!signal.aborted) message.hidden = true;
|
|
382
|
-
};
|
|
383
|
-
page.append(current.element);
|
|
384
|
-
current.load().catch(error => {
|
|
385
|
-
if (!signal.aborted) message.textContent = error.message;
|
|
386
|
-
});
|
|
387
|
-
return page;
|
|
231
|
+
page = new PageController(container, home, {
|
|
232
|
+
host,
|
|
233
|
+
menu,
|
|
234
|
+
title: config.title,
|
|
235
|
+
logo: config.logo ? new URL(config.logo, jsonURL).href : undefined,
|
|
236
|
+
logoText: config.logoText,
|
|
237
|
+
logoHref: new URL(config.logoHref || "/", jsonURL).href,
|
|
238
|
+
modules: new Map(config.sections.flatMap(section => section.items.filter(item => item.module).map(item => [item.module, item]))),
|
|
239
|
+
links,
|
|
388
240
|
});
|
|
241
|
+
navigation = page.navigation;
|
|
389
242
|
for (const [index, link] of links.entries()) {
|
|
390
243
|
link.addEventListener(
|
|
391
244
|
"click",
|
|
@@ -394,16 +247,10 @@ export async function startHome() {
|
|
|
394
247
|
event.preventDefault();
|
|
395
248
|
switch (link.dataset.target) {
|
|
396
249
|
case "internal":
|
|
397
|
-
navigation.open(
|
|
250
|
+
navigation.open(`$link/${index}`);
|
|
398
251
|
break;
|
|
399
252
|
default:
|
|
400
|
-
|
|
401
|
-
Promise.resolve(host.openURL(link.href, link.dataset.target)).catch(error => {
|
|
402
|
-
if (!destroyed) host.notice({ kind: "error", message: error.message });
|
|
403
|
-
});
|
|
404
|
-
} catch (error) {
|
|
405
|
-
if (!destroyed) host.notice({ kind: "error", message: error.message });
|
|
406
|
-
}
|
|
253
|
+
page.openURL(link.href, link.dataset.target);
|
|
407
254
|
break;
|
|
408
255
|
}
|
|
409
256
|
},
|
|
@@ -419,7 +266,6 @@ export async function startHome() {
|
|
|
419
266
|
return { button, status };
|
|
420
267
|
});
|
|
421
268
|
const probe = () => {
|
|
422
|
-
update();
|
|
423
269
|
if (navigation.current) return;
|
|
424
270
|
for (const { button, status } of statuses) status.check(`/api/${encodeURIComponent(button.dataset.module)}`);
|
|
425
271
|
};
|
package/src/browser/mount.mjs
CHANGED
|
@@ -1,103 +1,62 @@
|
|
|
1
|
+
import { ActionMenu } from "./ActionMenu.mjs";
|
|
1
2
|
import { normalizeBoxJs } from "./boxjs.mjs";
|
|
2
|
-
import { element
|
|
3
|
-
import {
|
|
3
|
+
import { element } from "./components.mjs";
|
|
4
|
+
import { browserHost, PageController } from "./page.mjs";
|
|
4
5
|
import { installDefaultStyles } from "./styles.mjs";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* 使用原始 BoxJS JSON 挂载设置页,共用内部页面控制器。
|
|
9
|
+
* Mount a settings page from raw BoxJS JSON through the shared internal page controller.
|
|
10
|
+
* @param {import("../index.js").BoxJSInput} boxjs 单模块 BoxJS JSON / Single-module BoxJS JSON.
|
|
11
|
+
* @returns {import("./index.js").MountedPreferences} 模块视图 / Module view.
|
|
9
12
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* Mount a settings page from raw BoxJS JSON.
|
|
26
|
-
* @param {import("../index.js").BoxJSInput} boxjs 单模块 BoxJS JSON / Single-module BoxJS JSON.
|
|
27
|
-
*/
|
|
28
|
-
constructor(boxjs) {
|
|
29
|
-
const definition = normalizeBoxJs(boxjs);
|
|
30
|
-
const metadata = definition.metadata ?? {};
|
|
31
|
-
const image = metadata.icon || metadata.icons?.[1] || metadata.icons?.[0];
|
|
32
|
-
if (image) resourceURL(image);
|
|
33
|
-
if (metadata.repo) resourceURL(metadata.repo);
|
|
34
|
-
|
|
35
|
-
this.#existing = document.querySelector("#preferences");
|
|
36
|
-
this.#root = this.#existing ?? element("main", "");
|
|
37
|
-
if (!this.#existing) {
|
|
38
|
-
this.#root.id = "preferences";
|
|
39
|
-
document.body.append(this.#root);
|
|
40
|
-
}
|
|
41
|
-
const styles = installDefaultStyles(document);
|
|
42
|
-
this.#base = styles.element;
|
|
43
|
-
this.#ownsBase = styles.owned;
|
|
44
|
-
this.#previousTitle = document.title;
|
|
45
|
-
this.#previousTheme = document.documentElement.dataset.theme;
|
|
46
|
-
this.#systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
47
|
-
this.#previousKeyboard = document.documentElement.style.getPropertyValue("--pp-keyboard-height");
|
|
48
|
-
this.#host = window.frameElement?.ownerDocument.documentElement;
|
|
49
|
-
this.#syncAppearance();
|
|
50
|
-
this.#systemTheme.addEventListener("change", this.#syncAppearance);
|
|
51
|
-
if (this.#host) {
|
|
52
|
-
this.#observer = new MutationObserver(this.#syncAppearance);
|
|
53
|
-
this.#observer.observe(this.#host, { attributes: true, attributeFilter: ["data-theme", "style"] });
|
|
54
|
-
}
|
|
55
|
-
document.title = metadata.name ?? definition.module;
|
|
56
|
-
try {
|
|
57
|
-
this.#root.replaceChildren();
|
|
58
|
-
this.#panel = new PreferencesPanel(this.#root, definition);
|
|
59
|
-
} catch (error) {
|
|
60
|
-
this.destroy();
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
13
|
+
export function mount(boxjs) {
|
|
14
|
+
const definition = normalizeBoxJs(boxjs);
|
|
15
|
+
const title = definition.metadata?.name ?? definition.module;
|
|
16
|
+
const existing = document.querySelector("#preferences");
|
|
17
|
+
const root = existing ?? element("main", "");
|
|
18
|
+
const previousTitle = document.title;
|
|
19
|
+
const previousKeyboard = document.documentElement.style.getPropertyValue("--pp-keyboard-height");
|
|
20
|
+
const previousHeight = document.documentElement.style.getPropertyValue("--pp-host-height");
|
|
21
|
+
const previousTheme = document.documentElement.dataset.theme;
|
|
22
|
+
const previousClass = document.documentElement.classList.contains("pp-page");
|
|
23
|
+
document.documentElement.classList.add("pp-page");
|
|
24
|
+
root.dataset.preferencePanesPages = "";
|
|
25
|
+
if (!existing) {
|
|
26
|
+
root.id = "preferences";
|
|
27
|
+
document.body.append(root);
|
|
63
28
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
document.documentElement.dataset.theme = theme;
|
|
73
|
-
if (this.#host) document.documentElement.style.setProperty("--pp-keyboard-height", this.#host.style.getPropertyValue("--pp-keyboard-height"));
|
|
29
|
+
const defaults = installDefaultStyles(document);
|
|
30
|
+
const menu = new ActionMenu(id => page.perform(id));
|
|
31
|
+
const host = browserHost({ menu, back: () => page.navigation.back() });
|
|
32
|
+
const page = new PageController(root, element("main", "pp-home"), { host, menu, title, modules: new Map([[definition.module, { name: title, boxjs }]]) });
|
|
33
|
+
if (!page.navigation.current) page.navigation.open(definition.module);
|
|
34
|
+
let destroyed = false;
|
|
35
|
+
const pagehide = event => {
|
|
36
|
+
if (!event.persisted) destroy();
|
|
74
37
|
};
|
|
75
|
-
|
|
38
|
+
window.addEventListener("pagehide", pagehide);
|
|
76
39
|
/**
|
|
77
|
-
*
|
|
78
|
-
* Release the module
|
|
40
|
+
* 释放模块、样式与监听器,并恢复调用方的文档状态。
|
|
41
|
+
* Release the module, styles and listeners and restore the caller's document state.
|
|
79
42
|
* @returns {void} 无返回值 / No return value.
|
|
80
43
|
*/
|
|
81
|
-
destroy() {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
else
|
|
91
|
-
document.documentElement.
|
|
44
|
+
function destroy() {
|
|
45
|
+
if (destroyed) return;
|
|
46
|
+
destroyed = true;
|
|
47
|
+
page.destroy();
|
|
48
|
+
window.removeEventListener("pagehide", pagehide);
|
|
49
|
+
if (defaults.owned) defaults.element.remove();
|
|
50
|
+
if (existing) {
|
|
51
|
+
root.replaceChildren();
|
|
52
|
+
delete root.dataset.preferencePanesPages;
|
|
53
|
+
} else root.remove();
|
|
54
|
+
if (!previousClass) document.documentElement.classList.remove("pp-page");
|
|
55
|
+
document.title = previousTitle;
|
|
56
|
+
document.documentElement.style.setProperty("--pp-keyboard-height", previousKeyboard);
|
|
57
|
+
document.documentElement.style.setProperty("--pp-host-height", previousHeight);
|
|
58
|
+
if (previousTheme === undefined) delete document.documentElement.dataset.theme;
|
|
59
|
+
else document.documentElement.dataset.theme = previousTheme;
|
|
92
60
|
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* 使用原始 BoxJS JSON 挂载设置页。
|
|
97
|
-
* Mount a settings page from raw BoxJS JSON.
|
|
98
|
-
* @param {import("../index.js").BoxJSInput} boxjs 单模块 BoxJS JSON / Single-module BoxJS JSON.
|
|
99
|
-
* @returns {import("./index.js").MountedPreferences} 模块视图 / Module view.
|
|
100
|
-
*/
|
|
101
|
-
export function mount(boxjs) {
|
|
102
|
-
return new PreferencesView(boxjs);
|
|
61
|
+
return { destroy };
|
|
103
62
|
}
|