@mantou/gem 2.3.0 → 2.3.2
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/AGENTS.md +150 -0
- package/dist/gem.js +6 -6
- package/dist/gem.mjs +6 -6
- package/elements/base/dialog.d.ts.map +1 -1
- package/elements/base/gesture.d.ts.map +1 -1
- package/elements/base/gesture.js +20 -1
- package/elements/base/gesture.js.map +1 -1
- package/elements/base/link.d.ts.map +1 -1
- package/elements/base/link.js +1 -1
- package/elements/base/link.js.map +1 -1
- package/elements/base/modal-factory.d.ts +20 -511
- package/elements/base/modal-factory.d.ts.map +1 -1
- package/elements/base/modal-factory.js +3 -2
- package/elements/base/modal-factory.js.map +1 -1
- package/elements/base/reflect.d.ts +7 -4
- package/elements/base/reflect.d.ts.map +1 -1
- package/elements/base/reflect.js +30 -4
- package/elements/base/reflect.js.map +1 -1
- package/elements/base/route.d.ts +3 -6
- package/elements/base/route.d.ts.map +1 -1
- package/elements/base/route.js +1 -1
- package/elements/base/route.js.map +1 -1
- package/elements/base/title.d.ts.map +1 -1
- package/elements/base/title.js +1 -1
- package/elements/base/title.js.map +1 -1
- package/helper/hmr.js.map +1 -1
- package/helper/i18n.d.ts +21 -17
- package/helper/i18n.d.ts.map +1 -1
- package/helper/i18n.js +172 -116
- package/helper/i18n.js.map +1 -1
- package/helper/logger.d.ts.map +1 -1
- package/helper/mediaquery.d.ts.map +1 -1
- package/helper/request.d.ts +7 -1
- package/helper/request.d.ts.map +1 -1
- package/helper/request.js.map +1 -1
- package/helper/theme.d.ts.map +1 -1
- package/helper/theme.js.map +1 -1
- package/lib/decorators.d.ts +3 -2
- package/lib/decorators.d.ts.map +1 -1
- package/lib/decorators.js +2 -1
- package/lib/decorators.js.map +1 -1
- package/lib/directive.d.ts.map +1 -1
- package/lib/element.d.ts +5 -1
- package/lib/element.d.ts.map +1 -1
- package/lib/element.js.map +1 -1
- package/lib/history.d.ts +46 -42
- package/lib/history.d.ts.map +1 -1
- package/lib/lit-html.d.ts.map +1 -1
- package/lib/lit-html.js.map +1 -1
- package/lib/reactive.d.ts +1 -4
- package/lib/reactive.d.ts.map +1 -1
- package/lib/reactive.js +1 -3
- package/lib/reactive.js.map +1 -1
- package/lib/repeat.d.ts +1 -1
- package/lib/repeat.d.ts.map +1 -1
- package/lib/repeat.js.map +1 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +6 -5
- package/src/elements/base/gesture.ts +20 -1
- package/src/elements/base/modal-factory.ts +22 -6
- package/src/elements/base/reflect.ts +30 -4
- package/src/elements/base/route.ts +2 -7
- package/src/helper/i18n.ts +184 -127
- package/src/lib/decorators.ts +2 -1
- package/src/lib/element.ts +5 -4
- package/src/lib/reactive.ts +1 -6
- package/src/lib/version.ts +1 -1
- package/src/test/elements/link.test.ts +12 -2
- package/src/test/elements/reflect.test.ts +20 -1
- package/src/test/elements/unsafe.test.ts +22 -0
- package/src/test/gem-element/advance.test.ts +87 -0
- package/src/test/utils.test.ts +4 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mantou/gem",
|
|
3
|
-
"version": "2.3.
|
|
4
|
-
"description": "💎
|
|
3
|
+
"version": "2.3.2",
|
|
4
|
+
"description": "💎 Build reusable custom elements and web apps on Web Components",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontend",
|
|
7
7
|
"web components",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"./helper/*": "./helper/*.js"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
+
"/AGENTS.md",
|
|
25
26
|
"/index.*",
|
|
26
27
|
"/dist/",
|
|
27
28
|
"/elements/",
|
|
@@ -33,14 +34,14 @@
|
|
|
33
34
|
"scripts": {
|
|
34
35
|
"docs": "gem-book docs",
|
|
35
36
|
"build:docs": "gem-book build docs",
|
|
36
|
-
"build:module": "
|
|
37
|
+
"build:module": "tsrs -p tsconfig.build.json",
|
|
37
38
|
"build:lib": "rslib build",
|
|
38
39
|
"build": "pnpm build:module && pnpm build:lib",
|
|
39
40
|
"start": "pnpm build:module --watch",
|
|
40
41
|
"test": "cross-env wtr"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
|
-
"@gemjs/config": "^2.1.
|
|
44
|
+
"@gemjs/config": "^2.1.4",
|
|
44
45
|
"@rslib/core": "^0.23.0",
|
|
45
46
|
"@swc/helpers": "^0.5.23",
|
|
46
47
|
"@types/chai": "^5.0.0",
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"@web/test-runner": "^0.18.1",
|
|
51
52
|
"chai": "^5.1.1",
|
|
52
53
|
"rimraf": "^3.0.2",
|
|
53
|
-
"
|
|
54
|
+
"tsrs": "^0.18.0-ts7.1.0-dev.20260929"
|
|
54
55
|
},
|
|
55
56
|
"author": "mantou132",
|
|
56
57
|
"license": "MIT",
|
|
@@ -137,7 +137,26 @@ const style = css`
|
|
|
137
137
|
&:where(:not([hidden])) {
|
|
138
138
|
display: block;
|
|
139
139
|
user-select: none;
|
|
140
|
-
touch-action:
|
|
140
|
+
touch-action: none;
|
|
141
|
+
}
|
|
142
|
+
/* 常用取值,兼容不支持在 touch-action 中使用 attr() 的浏览器 */
|
|
143
|
+
&:where([touch-action='auto']) {
|
|
144
|
+
touch-action: auto;
|
|
145
|
+
}
|
|
146
|
+
&:where([touch-action='manipulation']) {
|
|
147
|
+
touch-action: manipulation;
|
|
148
|
+
}
|
|
149
|
+
&:where([touch-action='pan-x']) {
|
|
150
|
+
touch-action: pan-x;
|
|
151
|
+
}
|
|
152
|
+
&:where([touch-action='pan-y']) {
|
|
153
|
+
touch-action: pan-y;
|
|
154
|
+
}
|
|
155
|
+
/* 支持任意取值;不带类型的 attr() 是字符串,对 touch-action 无效 */
|
|
156
|
+
@supports (touch-action: attr(touch-action type(*), none)) {
|
|
157
|
+
&:where([touch-action]) {
|
|
158
|
+
touch-action: attr(touch-action type(*), none);
|
|
159
|
+
}
|
|
141
160
|
}
|
|
142
161
|
}
|
|
143
162
|
`;
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import { aria, mounted, shadow } from '../../lib/decorators';
|
|
2
2
|
import { history } from '../../lib/history';
|
|
3
3
|
import { GemElement } from '../../lib/reactive';
|
|
4
|
+
import type { Store } from '../../lib/store';
|
|
4
5
|
import { connect, createStore } from '../../lib/store';
|
|
5
6
|
|
|
6
7
|
const open = Symbol('open mark');
|
|
8
|
+
// 静态方法返回不可伪造的值,防止子类覆盖
|
|
9
|
+
const final = Symbol();
|
|
10
|
+
|
|
11
|
+
type ModalElement = GemElement & { label: string };
|
|
12
|
+
|
|
13
|
+
export interface ModalClass<T> extends Omit<typeof GemElement, 'prototype'> {
|
|
14
|
+
new (): ModalElement;
|
|
15
|
+
prototype: ModalElement;
|
|
16
|
+
inertStore: HTMLElement[];
|
|
17
|
+
instance: GemElement | null;
|
|
18
|
+
store: Store<{ [open]: boolean } & T>;
|
|
19
|
+
readonly isOpen: boolean;
|
|
20
|
+
open(opts: T): typeof final;
|
|
21
|
+
close(): typeof final;
|
|
22
|
+
closeHandle(): typeof final;
|
|
23
|
+
shouldClose(): boolean;
|
|
24
|
+
}
|
|
7
25
|
|
|
8
26
|
/**
|
|
9
27
|
* 需要使用范型,所以导出一个函数,用来*创建*类似 Confirm 元素的基类;
|
|
@@ -13,9 +31,7 @@ const open = Symbol('open mark');
|
|
|
13
31
|
* 使用 `mounted`/`unmounted` 定义 `open`/`close` 回调;
|
|
14
32
|
* 模拟 top layer:https://github.com/whatwg/html/issues/4633.
|
|
15
33
|
*/
|
|
16
|
-
export function createModalClass<T extends Record<string, unknown>>(options: T) {
|
|
17
|
-
const final = Symbol();
|
|
18
|
-
|
|
34
|
+
export function createModalClass<T extends Record<string, unknown>>(options: T): ModalClass<T> {
|
|
19
35
|
@shadow()
|
|
20
36
|
@aria({ role: 'alertdialog', ariaModal: 'true' })
|
|
21
37
|
class ModalElement extends GemElement {
|
|
@@ -40,7 +56,7 @@ export function createModalClass<T extends Record<string, unknown>>(options: T)
|
|
|
40
56
|
* 自带 100ms 延迟,以允许在其他 Dialog 的 `shouldClose` 中调用此方法;
|
|
41
57
|
* 浏览器 history 为异步 API,需要设置较长延迟;
|
|
42
58
|
*/
|
|
43
|
-
static open(opts: T) {
|
|
59
|
+
static open(opts: T): typeof final {
|
|
44
60
|
const instance = new this();
|
|
45
61
|
this.instance = instance;
|
|
46
62
|
this.inertStore = ([...document.body.children] as HTMLElement[]).filter((e) => !e.inert);
|
|
@@ -63,7 +79,7 @@ export function createModalClass<T extends Record<string, unknown>>(options: T)
|
|
|
63
79
|
* @final
|
|
64
80
|
* UI 中执行关闭
|
|
65
81
|
*/
|
|
66
|
-
static close() {
|
|
82
|
+
static close(): typeof final {
|
|
67
83
|
history.back();
|
|
68
84
|
return final;
|
|
69
85
|
}
|
|
@@ -72,7 +88,7 @@ export function createModalClass<T extends Record<string, unknown>>(options: T)
|
|
|
72
88
|
* @final
|
|
73
89
|
* history 中自动调用
|
|
74
90
|
*/
|
|
75
|
-
static closeHandle() {
|
|
91
|
+
static closeHandle(): typeof final {
|
|
76
92
|
this.inertStore.forEach((e) => (e.inert = false));
|
|
77
93
|
this.instance?.remove();
|
|
78
94
|
this.store({ [open]: false, ...options });
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* 把子节点移动到 `target`(默认 `document.head`)中,用开始和结束注释标记这些节点,
|
|
3
|
+
* 修改 `target` 或者卸载时一起移动或移除
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* 比如 duoyun-ui 文档站加载示例元素时把 GemReflectElement 渲染的元素清除
|
|
5
|
+
* 服务端渲染时不执行 effect,客户端水合后才移动,使用 `getLogicalParent` 从被移动的节点找到 reflect 元素
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { attribute, effect, property, willMount } from '../../lib/decorators';
|
|
@@ -25,6 +24,31 @@ const getReflectFragment = (startNode: Comment) => {
|
|
|
25
24
|
return result;
|
|
26
25
|
};
|
|
27
26
|
|
|
27
|
+
// 移动到的目标元素和片段开始注释对应的 reflect 元素,向上查找元素时跨越 reflect
|
|
28
|
+
const targets = new WeakSet<Node>();
|
|
29
|
+
const startNodes = new WeakMap<Node, GemReflectElement>();
|
|
30
|
+
|
|
31
|
+
// 向前查找片段的开始注释,遇到其他片段的结束注释时不在片段中
|
|
32
|
+
const getReflectElement = (node: Node) => {
|
|
33
|
+
for (let n = node.previousSibling; n; n = n.previousSibling) {
|
|
34
|
+
const ele = startNodes.get(n);
|
|
35
|
+
if (ele) return ele;
|
|
36
|
+
if (n.nodeValue === END) return;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 节点的逻辑父元素:被 reflect 移动的节点是 reflect 元素,ShadowRoot 中顶层的节点是宿主,其他是父元素
|
|
42
|
+
*/
|
|
43
|
+
export function getLogicalParent(node: Node): Element | null {
|
|
44
|
+
const parent = node.parentNode;
|
|
45
|
+
if (parent && targets.has(parent)) {
|
|
46
|
+
const ele = getReflectElement(node);
|
|
47
|
+
if (ele) return ele;
|
|
48
|
+
}
|
|
49
|
+
return parent instanceof ShadowRoot ? parent.host : node.parentElement;
|
|
50
|
+
}
|
|
51
|
+
|
|
28
52
|
export class GemReflectElement extends GemElement {
|
|
29
53
|
@attribute method: 'prepend' | 'append';
|
|
30
54
|
|
|
@@ -45,6 +69,8 @@ export class GemReflectElement extends GemElement {
|
|
|
45
69
|
|
|
46
70
|
@effect((i) => [i.target])
|
|
47
71
|
#changeTarget = () => {
|
|
72
|
+
targets.add(this.target);
|
|
73
|
+
startNodes.set(this.#startNode, this);
|
|
48
74
|
(this.#childNodes || [this.#startNode, ...this.childNodes, this.#endNode]).forEach((node, index, arr) => {
|
|
49
75
|
const prev = arr[index - 1];
|
|
50
76
|
if (prev) {
|
|
@@ -12,12 +12,6 @@ import { addListener } from '../../lib/utils';
|
|
|
12
12
|
type Params = Record<string, string>;
|
|
13
13
|
type Matcher = (path: string) => Params | null | undefined;
|
|
14
14
|
|
|
15
|
-
declare global {
|
|
16
|
-
// https://bugzilla.mozilla.org/show_bug.cgi?id=1731418
|
|
17
|
-
// https://github.com/WebKit/standards-positions/issues/61
|
|
18
|
-
var URLPattern: any;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
15
|
const UNNAMED_PARAM_PREFIX = '_';
|
|
22
16
|
|
|
23
17
|
// 不支持 `URLPattern` 时转换成正则
|
|
@@ -56,7 +50,8 @@ function createMatcher(pattern: string): Matcher {
|
|
|
56
50
|
const urlPattern = new URLPattern({ pathname: pattern });
|
|
57
51
|
// 参数前的 `/` 可能是可选的,例如 `/a/:b?` 匹配 `/a`
|
|
58
52
|
const prefix = literal.replace(/\/$/, '');
|
|
59
|
-
return (path) =>
|
|
53
|
+
return (path) =>
|
|
54
|
+
path.startsWith(prefix) ? (urlPattern.exec({ pathname: path })?.pathname.groups as Params | undefined) : null;
|
|
60
55
|
}
|
|
61
56
|
|
|
62
57
|
// pattern 来自路由定义,数量有限
|
package/src/helper/i18n.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { RouteTrigger } from '../elements/base/route';
|
|
|
2
2
|
import type { UpdateHistoryParams } from '../lib/history';
|
|
3
3
|
import type { TemplateResult } from '../lib/lit-html';
|
|
4
4
|
import { html, UpdateToken } from '../lib/reactive';
|
|
5
|
-
import { isolate } from '../lib/server';
|
|
5
|
+
import { isolate, isServer, ssrHooks } from '../lib/server';
|
|
6
6
|
import { GemError } from '../lib/utils';
|
|
7
7
|
import { logger } from './logger';
|
|
8
8
|
|
|
@@ -26,7 +26,8 @@ export function splice(rawValue: string, ...rest: (((s: string) => TemplateResul
|
|
|
26
26
|
return html(templateArr, ...values);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// 正在创建子模块,构造函数中用来区分主模块
|
|
30
|
+
let creatingSubModule = false;
|
|
30
31
|
|
|
31
32
|
type Msg =
|
|
32
33
|
| {
|
|
@@ -35,13 +36,20 @@ type Msg =
|
|
|
35
36
|
}
|
|
36
37
|
| string;
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
type I18nJSON<T> = {
|
|
39
40
|
currentLanguage: string;
|
|
40
41
|
currentLanguagePack: Partial<T>;
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* 语言包加载函数,例如 `() => import('./zh.json')`,打包时每个语言是单独的文件,只加载用到的语言
|
|
46
|
+
*
|
|
47
|
+
* 类型只从 `default` 推断,否则模块对象本身也会被推断成语言包
|
|
48
|
+
*/
|
|
49
|
+
type PackLoader<T> = () => Promise<{ default: Partial<T> } | NoInfer<Partial<T>>>;
|
|
50
|
+
|
|
43
51
|
interface Resources<T> {
|
|
44
|
-
[key: string]: Partial<T> |
|
|
52
|
+
[key: string]: Partial<T> | PackLoader<T>;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
interface I18nOptions<T> {
|
|
@@ -50,12 +58,12 @@ interface I18nOptions<T> {
|
|
|
50
58
|
// 可以在构造函数中指定,但会被 URL 中的语言参数覆盖
|
|
51
59
|
currentLanguage?: string;
|
|
52
60
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
61
|
+
* 缓存用户选择的语言,`true` 表示在修改语言时自动缓存,`manual` 时调用 `setCache` 缓存
|
|
62
|
+
*
|
|
63
|
+
* 同时缓存加载函数加载的语言包,下次加载完成前先使用缓存,加载完成后更新
|
|
55
64
|
*/
|
|
56
65
|
cache?: boolean | 'manual';
|
|
57
|
-
// lib
|
|
58
|
-
// 也用来作为 prerender 的 script type
|
|
66
|
+
// 缓存和 SSR 输出的 key,lib 需要设置,避免和应用中的 i18n 冲突
|
|
59
67
|
cachePrefix?: string;
|
|
60
68
|
// 为 `path` 时页面的路由不能和语言代码相同
|
|
61
69
|
urlParamsType?: 'path' | 'querystring' | 'ccTLD' | 'gTLD';
|
|
@@ -72,8 +80,12 @@ export class I18n<T = Record<string, Msg>> implements RouteTrigger {
|
|
|
72
80
|
urlParamsName?: string;
|
|
73
81
|
onChange?: (currentLanguage: string) => void;
|
|
74
82
|
|
|
75
|
-
// SSR
|
|
76
|
-
#state = isolate({ currentLanguage: '' });
|
|
83
|
+
// SSR 时每个请求使用独立的语言,`pending` 是正在切换的语言
|
|
84
|
+
#state = isolate({ currentLanguage: '', pending: undefined as Promise<string> | undefined });
|
|
85
|
+
|
|
86
|
+
// 子模块跟随主模块切换语言,不修改文档的语言,也不缓存语言,只缓存语言包
|
|
87
|
+
#isSub = creatingSubModule;
|
|
88
|
+
#subModules = new Set<I18n<any>>();
|
|
77
89
|
|
|
78
90
|
get currentLanguage() {
|
|
79
91
|
return this.#state.currentLanguage;
|
|
@@ -83,14 +95,23 @@ export class I18n<T = Record<string, Msg>> implements RouteTrigger {
|
|
|
83
95
|
this.#state.currentLanguage = lang;
|
|
84
96
|
}
|
|
85
97
|
|
|
98
|
+
/** 等待切换语言完成,包括加载语言包和所有子模块,例如渲染刚加载的模块前 */
|
|
99
|
+
get ready(): Promise<unknown> {
|
|
100
|
+
return Promise.all([this.#state.pending, ...[...this.#subModules].map((i18n) => i18n.ready)]);
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
// 兼容 `<gem-route>`
|
|
87
104
|
replace = ({ path }: UpdateHistoryParams) => this.setLanguage(path!);
|
|
88
105
|
getParams = () => ({ path: this.currentLanguage });
|
|
89
106
|
|
|
90
|
-
get #
|
|
107
|
+
get #cacheKey() {
|
|
91
108
|
return `${this.cachePrefix}:current`;
|
|
92
109
|
}
|
|
93
110
|
|
|
111
|
+
get #storeKey() {
|
|
112
|
+
return `i18n:${this.cachePrefix}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
94
115
|
get #urlParamsLang() {
|
|
95
116
|
const { hostname, pathname, search } = location;
|
|
96
117
|
const parts = hostname.split('.');
|
|
@@ -109,21 +130,27 @@ export class I18n<T = Record<string, Msg>> implements RouteTrigger {
|
|
|
109
130
|
}
|
|
110
131
|
|
|
111
132
|
get #cacheLang() {
|
|
112
|
-
return (this.cache && localStorage.getItem(this.#
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
get #isMain(): boolean {
|
|
116
|
-
return !modules.has(this);
|
|
133
|
+
return (!this.#isSub && this.cache && localStorage.getItem(this.#cacheKey)) || '';
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
set #lang(lang: string) {
|
|
120
|
-
if (this.#
|
|
121
|
-
document.documentElement.lang = lang;
|
|
122
|
-
}
|
|
137
|
+
if (!this.#isSub) document.documentElement.lang = lang;
|
|
123
138
|
this.currentLanguage = lang;
|
|
124
139
|
this.onChange?.(lang);
|
|
125
140
|
}
|
|
126
141
|
|
|
142
|
+
/**
|
|
143
|
+
* SSR 时使用过的模块记录到 store 中,渲染结束时读取,输出当前语言的语言包,
|
|
144
|
+
* 客户端构造时恢复,不需要再加载
|
|
145
|
+
*/
|
|
146
|
+
#ssrData = Object.defineProperties(
|
|
147
|
+
{},
|
|
148
|
+
{
|
|
149
|
+
currentLanguage: { enumerable: true, get: () => this.currentLanguage },
|
|
150
|
+
currentLanguagePack: { enumerable: true, get: () => this.#getCompletePack() },
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
127
154
|
#inConstructor = true;
|
|
128
155
|
constructor(options: I18nOptions<T>) {
|
|
129
156
|
if (!options.resources[options.fallbackLanguage]) {
|
|
@@ -132,35 +159,49 @@ export class I18n<T = Record<string, Msg>> implements RouteTrigger {
|
|
|
132
159
|
|
|
133
160
|
Object.assign<I18n<T>, I18nOptions<T>>(this as I18n<T>, options);
|
|
134
161
|
|
|
135
|
-
|
|
162
|
+
const ssrData = isServer ? undefined : (ssrHooks.createStore?.(this.#storeKey) as I18nJSON<T> | undefined);
|
|
163
|
+
// 已经包含后备语言中的消息
|
|
164
|
+
if (ssrData) this.resources[ssrData.currentLanguage] = ssrData.currentLanguagePack;
|
|
136
165
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
currentLanguage = prerenderData.currentLanguage;
|
|
142
|
-
this.resources[prerenderData.currentLanguage] = prerenderData.currentLanguagePack;
|
|
143
|
-
} catch {
|
|
144
|
-
//
|
|
145
|
-
}
|
|
146
|
-
}
|
|
166
|
+
// 子模块使用主模块当前的语言,可能和服务端输出时不同,例如客户端切换过语言
|
|
167
|
+
const lang = this.#isSub
|
|
168
|
+
? this.currentLanguage || ssrData?.currentLanguage
|
|
169
|
+
: ssrData?.currentLanguage || this.#urlParamsLang || this.currentLanguage || this.#cacheLang;
|
|
147
170
|
|
|
148
|
-
if (
|
|
149
|
-
this.#lang =
|
|
150
|
-
this.setLanguage(
|
|
171
|
+
if (lang) {
|
|
172
|
+
this.#lang = lang;
|
|
173
|
+
this.setLanguage(lang);
|
|
151
174
|
} else {
|
|
152
175
|
this.resetLanguage();
|
|
153
176
|
}
|
|
154
177
|
this.#inConstructor = false;
|
|
155
178
|
}
|
|
156
179
|
|
|
180
|
+
#getPack(lang: string): Record<string, Msg> | undefined {
|
|
181
|
+
const data = this.resources[lang];
|
|
182
|
+
return data && typeof data === 'object' ? (data as any) : undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 当前语言包,包含后备语言中的消息,服务端输出后客户端不需要再加载后备语言 */
|
|
186
|
+
#getCompletePack() {
|
|
187
|
+
const pack: Record<string, Msg> = { ...this.#getPack(this.fallbackLanguage) };
|
|
188
|
+
for (const [key, msg] of Object.entries(this.#getPack(this.currentLanguage) || {})) {
|
|
189
|
+
if (msg) pack[key] = msg;
|
|
190
|
+
}
|
|
191
|
+
return pack as Partial<T>;
|
|
192
|
+
}
|
|
193
|
+
|
|
157
194
|
get(s: keyof T): string;
|
|
158
195
|
get(s: keyof T, ...rest: (((str: string) => TemplateResult) | string)[]): TemplateResult;
|
|
159
196
|
get(s: keyof T, ...rest: (((str: string) => TemplateResult) | string)[]) {
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
const
|
|
197
|
+
if (isServer) ssrHooks.updateStore?.(this.#storeKey, this.#ssrData);
|
|
198
|
+
const key = s as string;
|
|
199
|
+
const currentPack = this.#getPack(this.currentLanguage);
|
|
200
|
+
const fallbackPack = this.#getPack(this.fallbackLanguage);
|
|
201
|
+
const msg = currentPack?.[key] || fallbackPack?.[key];
|
|
202
|
+
// 客户端只加载当前语言,缺少消息时才加载后备语言
|
|
203
|
+
if (!msg && currentPack && !fallbackPack) this.#loadFallback();
|
|
204
|
+
const rawValue: string = (typeof msg === 'object' ? msg.message : msg) || `[${key}]`;
|
|
164
205
|
if (!rest.length) return rawValue;
|
|
165
206
|
return splice(rawValue, ...rest);
|
|
166
207
|
}
|
|
@@ -175,116 +216,132 @@ export class I18n<T = Record<string, Msg>> implements RouteTrigger {
|
|
|
175
216
|
return this.fallbackLanguage;
|
|
176
217
|
}
|
|
177
218
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
let pack: Partial<T>;
|
|
183
|
-
const data = this.resources[lang];
|
|
184
|
-
if (!data) {
|
|
185
|
-
logger.warn(`${this.cachePrefix}: not found \`${lang}\``);
|
|
186
|
-
return this.resetLanguage();
|
|
187
|
-
}
|
|
219
|
+
#packCacheKey(lang: string) {
|
|
220
|
+
return `${this.cachePrefix}:pack:${lang}`;
|
|
221
|
+
}
|
|
188
222
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const pkg = await (await fetch(data)).json();
|
|
196
|
-
if (this.cache) {
|
|
197
|
-
// 移除之前的版本缓存
|
|
198
|
-
for (let i = 0; i < localStorage.length; i++) {
|
|
199
|
-
const key = localStorage.key(i) as string;
|
|
200
|
-
if (key.startsWith(prefix)) {
|
|
201
|
-
localStorage.removeItem(key);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
localStorage.setItem(localKey, JSON.stringify(pkg));
|
|
205
|
-
}
|
|
206
|
-
return pkg;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
if (localPackString) {
|
|
210
|
-
try {
|
|
211
|
-
pack = JSON.parse(localPackString);
|
|
212
|
-
} catch {
|
|
213
|
-
pack = await fetchPack();
|
|
214
|
-
}
|
|
215
|
-
} else {
|
|
216
|
-
pack = await fetchPack();
|
|
217
|
-
}
|
|
218
|
-
} else {
|
|
219
|
-
pack = data;
|
|
223
|
+
#readPackCache(lang: string): Partial<T> | undefined {
|
|
224
|
+
if (!this.cache || isServer) return;
|
|
225
|
+
try {
|
|
226
|
+
return JSON.parse(localStorage.getItem(this.#packCacheKey(lang)) || 'null') || undefined;
|
|
227
|
+
} catch {
|
|
228
|
+
//
|
|
220
229
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 缓存语言包,只保留正在使用的语言,返回内容是否和缓存不同 */
|
|
233
|
+
#writePackCache(lang: string, pack: Partial<T>) {
|
|
234
|
+
if (!this.cache || isServer) return false;
|
|
235
|
+
const key = this.#packCacheKey(lang);
|
|
236
|
+
const json = JSON.stringify(pack);
|
|
237
|
+
try {
|
|
238
|
+
if (localStorage.getItem(key) === json) return false;
|
|
239
|
+
const keep = new Set([lang, this.currentLanguage, this.fallbackLanguage].map((e) => this.#packCacheKey(e)));
|
|
240
|
+
const prefix = this.#packCacheKey('');
|
|
241
|
+
for (const k of Object.keys(localStorage)) {
|
|
242
|
+
if (k.startsWith(prefix) && !keep.has(k)) localStorage.removeItem(k);
|
|
231
243
|
}
|
|
244
|
+
localStorage.setItem(key, json);
|
|
245
|
+
} catch {
|
|
246
|
+
// 例如超出存储限制
|
|
232
247
|
}
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
233
250
|
|
|
234
|
-
|
|
235
|
-
|
|
251
|
+
/** 已经加载时同步返回,避免构造函数中异步更新,有缓存时先使用缓存,加载完成后更新 */
|
|
252
|
+
#loadPack(lang: string): Partial<T> | Promise<Partial<T>> {
|
|
253
|
+
const data = this.resources[lang];
|
|
254
|
+
if (typeof data !== 'function') return data;
|
|
255
|
+
const cached = this.#readPackCache(lang);
|
|
256
|
+
const loading = data().then((mod: any) => {
|
|
257
|
+
const pack = (mod && typeof mod.default === 'object' ? mod.default : mod) as Partial<T>;
|
|
258
|
+
this.resources[lang] = pack;
|
|
259
|
+
if (this.#writePackCache(lang, pack) && cached) this.#updateElements();
|
|
260
|
+
return pack;
|
|
261
|
+
});
|
|
262
|
+
if (!cached) return loading;
|
|
263
|
+
// 加载失败时继续使用缓存
|
|
264
|
+
loading.catch((err) => logger.warn(`${this.cachePrefix}: failed to load \`${lang}\``, err));
|
|
265
|
+
this.resources[lang] = cached;
|
|
266
|
+
return cached;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#fallbackLoading?: Promise<unknown>;
|
|
270
|
+
#loadFallback() {
|
|
271
|
+
this.#fallbackLoading ||= Promise.resolve(this.#loadPack(this.fallbackLanguage)).then(() => this.#updateElements());
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 更新所有元素,可能会更新许多不必要的元素
|
|
275
|
+
#updateElements() {
|
|
276
|
+
const temp: Element[] = [document.documentElement];
|
|
277
|
+
while (temp.length) {
|
|
278
|
+
const element = temp.pop()!;
|
|
279
|
+
(element as any)[UpdateToken]?.();
|
|
280
|
+
// 和 gem 更新机制一样使用深度优先
|
|
281
|
+
temp.push(...[...element.children, ...(element.shadowRoot?.children || [])].reverse());
|
|
236
282
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
setLanguage(lang: string): Promise<string> {
|
|
286
|
+
const promise = this.#setLanguage(lang);
|
|
287
|
+
this.#state.pending = promise;
|
|
288
|
+
return promise;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async #setLanguage(lang: string): Promise<string> {
|
|
292
|
+
if (!this.resources[lang]) {
|
|
293
|
+
logger.warn(`${this.cachePrefix}: not found \`${lang}\``);
|
|
294
|
+
return this.resetLanguage();
|
|
240
295
|
}
|
|
296
|
+
const subModules = [...this.#subModules].map((i18n) => i18n.setLanguage(lang));
|
|
297
|
+
|
|
298
|
+
// 服务端渲染需要同步读取后备语言中的消息,输出的语言包也包含后备语言的消息
|
|
299
|
+
const langs = isServer && lang !== this.fallbackLanguage ? [lang, this.fallbackLanguage] : [lang];
|
|
300
|
+
const packs = langs.map((e) => this.#loadPack(e));
|
|
301
|
+
if (packs.some((e) => e instanceof Promise)) await Promise.all(packs);
|
|
302
|
+
|
|
303
|
+
if (!this.#inConstructor) this.#updateElements();
|
|
304
|
+
if (lang !== this.currentLanguage) this.#lang = lang;
|
|
305
|
+
if (this.cache === true) this.setCache();
|
|
306
|
+
await Promise.all(subModules);
|
|
241
307
|
return lang;
|
|
242
308
|
}
|
|
243
309
|
|
|
244
310
|
resetLanguage() {
|
|
245
|
-
if (this.#
|
|
246
|
-
modules.forEach((i18n) => i18n.resetLanguage());
|
|
247
|
-
}
|
|
248
|
-
this.#lang = this.fallbackLanguage;
|
|
249
|
-
if (this.cache) {
|
|
250
|
-
localStorage.removeItem(this.#cacheCurrentKey);
|
|
251
|
-
}
|
|
311
|
+
if (this.cache) localStorage.removeItem(this.#cacheKey);
|
|
252
312
|
return this.setLanguage(this.detectLanguage());
|
|
253
313
|
}
|
|
254
314
|
|
|
255
|
-
// 一般用在 `onChange`
|
|
315
|
+
// 一般用在 `onChange` 中,子模块的语言来自主模块,不缓存
|
|
256
316
|
setCache() {
|
|
257
|
-
if (this.#
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
localStorage.setItem(this.#cacheCurrentKey, this.currentLanguage);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
createSubModule<K = Record<string, Msg>>(name: string, resources: Resources<K>) {
|
|
264
|
-
const module = new I18n<K>({
|
|
265
|
-
resources,
|
|
266
|
-
cachePrefix: `${this.cachePrefix}-${name}`,
|
|
267
|
-
currentLanguage: this.currentLanguage,
|
|
268
|
-
fallbackLanguage: this.fallbackLanguage,
|
|
269
|
-
cache: this.cache,
|
|
270
|
-
});
|
|
271
|
-
modules.add(module);
|
|
272
|
-
return module;
|
|
317
|
+
if (this.#isSub) return;
|
|
318
|
+
localStorage.setItem(this.#cacheKey, this.currentLanguage);
|
|
273
319
|
}
|
|
274
320
|
|
|
275
321
|
/**
|
|
322
|
+
* 子模块跟随主模块切换语言,可以按路由分割语言包
|
|
323
|
+
*
|
|
276
324
|
* @example
|
|
277
325
|
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* `
|
|
326
|
+
* const pageI18n = i18n.createSubModule('page', {
|
|
327
|
+
* en: () => import('./en.json'),
|
|
328
|
+
* zh: () => import('./zh.json'),
|
|
329
|
+
* });
|
|
283
330
|
*/
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
331
|
+
createSubModule<K = Record<string, Msg>>(name: string, resources: Resources<K>) {
|
|
332
|
+
creatingSubModule = true;
|
|
333
|
+
try {
|
|
334
|
+
const module = new I18n<K>({
|
|
335
|
+
resources,
|
|
336
|
+
cachePrefix: `${this.cachePrefix}-${name}`,
|
|
337
|
+
currentLanguage: this.currentLanguage,
|
|
338
|
+
fallbackLanguage: this.fallbackLanguage,
|
|
339
|
+
cache: this.cache,
|
|
340
|
+
});
|
|
341
|
+
this.#subModules.add(module);
|
|
342
|
+
return module;
|
|
343
|
+
} finally {
|
|
344
|
+
creatingSubModule = false;
|
|
345
|
+
}
|
|
289
346
|
}
|
|
290
347
|
}
|
package/src/lib/decorators.ts
CHANGED
|
@@ -418,7 +418,8 @@ export type Emitter<T = any> = <_Listener = EmitterListener<T>>(
|
|
|
418
418
|
|
|
419
419
|
/**
|
|
420
420
|
* 定义一个事件发射器,类似 `HTMLElement.click`,
|
|
421
|
-
*
|
|
421
|
+
* 调用时将在微任务中发送一个和字段名称的烤串式格式的自定义事件,
|
|
422
|
+
* 避免同步触发时祖先元素还没挂载好(https://github.com/mantou132/gem/issues/203)
|
|
422
423
|
*
|
|
423
424
|
* @example
|
|
424
425
|
* ```ts
|
package/src/lib/element.ts
CHANGED
|
@@ -13,11 +13,12 @@ export { createRef, mathml, svg, TemplateResult } from './lit-html';
|
|
|
13
13
|
export * from './reactive';
|
|
14
14
|
export { repeat } from './repeat';
|
|
15
15
|
|
|
16
|
+
type HookExports = typeof reactiveExports & typeof decoratorsExports & typeof storeExports & typeof versionExports;
|
|
17
|
+
|
|
16
18
|
declare global {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
| undefined;
|
|
19
|
+
/** devtools 注入的钩子,其他包可以扩展,例如 gem-ssr */
|
|
20
|
+
interface GemDevToolsHook extends HookExports {}
|
|
21
|
+
var __GEM_DEVTOOLS__HOOK__: GemDevToolsHook | Record<string, never> | undefined;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
// 只记录第一次定义,往往是最外层 App
|