@mantou/gem 2.2.15 → 2.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/dist/gem.js +7 -7
- package/dist/gem.mjs +7 -7
- package/elements/base/dialog.d.ts.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 -510
- 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 +11 -9
- package/elements/base/route.d.ts.map +1 -1
- package/elements/base/route.js +84 -44
- 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 +8 -4
- package/helper/theme.d.ts.map +1 -1
- package/helper/theme.js +12 -9
- 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 +29 -44
- package/lib/decorators.js.map +1 -1
- package/lib/directive.d.ts.map +1 -1
- package/lib/history.d.ts +46 -42
- package/lib/history.d.ts.map +1 -1
- package/lib/history.js +19 -37
- package/lib/history.js.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 +7 -9
- package/lib/reactive.d.ts.map +1 -1
- package/lib/reactive.js +86 -115
- 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/server.d.ts +12 -0
- package/lib/server.d.ts.map +1 -1
- package/lib/server.js +2 -0
- package/lib/server.js.map +1 -1
- package/lib/store.d.ts +0 -6
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +5 -17
- package/lib/store.js.map +1 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.d.ts.map +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- package/package.json +7 -8
- package/src/elements/base/modal-factory.ts +22 -6
- package/src/elements/base/reflect.ts +30 -4
- package/src/elements/base/route.ts +80 -50
- package/src/helper/i18n.ts +184 -127
- package/src/helper/theme.ts +16 -9
- package/src/lib/decorators.ts +36 -49
- package/src/lib/history.ts +19 -32
- package/src/lib/reactive.ts +90 -126
- package/src/lib/server.ts +14 -0
- package/src/lib/store.ts +3 -19
- package/src/lib/version.ts +1 -1
- package/src/test/elements/gesture.test.ts +13 -5
- package/src/test/elements/link.test.ts +12 -2
- package/src/test/elements/reflect.test.ts +20 -1
- package/src/test/elements/route.test.ts +102 -0
- package/src/test/elements/title.test.ts +7 -0
- package/src/test/elements/unsafe.test.ts +22 -0
- package/src/test/gem-element/advance.test.ts +106 -0
- package/src/test/gem-element/basic.test.ts +13 -0
- package/src/test/history.test.ts +19 -0
- package/src/test/theme.test.ts +8 -1
- package/src/test/utils.test.ts +10 -0
- package/src/test/utils.ts +56 -0
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/helper/theme.ts
CHANGED
|
@@ -26,8 +26,11 @@ export function getThemeProps<T>(theme: Theme<T>) {
|
|
|
26
26
|
return themePropsMap.get(theme) as Theme<T>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function createThemeFromProps<T extends Record<string, unknown>>(
|
|
30
|
-
|
|
29
|
+
function createThemeFromProps<T extends Record<string, unknown>>(
|
|
30
|
+
themeObj: T,
|
|
31
|
+
props: Record<string, string> = {},
|
|
32
|
+
salt = randomStr(),
|
|
33
|
+
) {
|
|
31
34
|
const styleSheet = css({})[SheetToken];
|
|
32
35
|
const store = createStore<T>(themeObj);
|
|
33
36
|
// 同步分配变量名,新增的键在更新后立即可读;样式内容随 store 异步更新
|
|
@@ -75,14 +78,17 @@ function createThemeFromProps<T extends Record<string, unknown>>(themeObj: T, pr
|
|
|
75
78
|
|
|
76
79
|
/**
|
|
77
80
|
* 用于 `@adoptedStyle(theme)`,类似 `css`
|
|
81
|
+
*
|
|
82
|
+
* `key` 用作 css 变量名的后缀,SSR 时让服务端和客户端的变量名一致,
|
|
83
|
+
* `swc-plugin-gem` 的 `ssr` 选项会为模块顶层的主题自动添加
|
|
78
84
|
*/
|
|
79
|
-
export function createScopedTheme<T extends Record<string, unknown>>(themeObj: T) {
|
|
80
|
-
return createThemeFromProps(themeObj);
|
|
85
|
+
export function createScopedTheme<T extends Record<string, unknown>>(themeObj: T, key?: string) {
|
|
86
|
+
return createThemeFromProps(themeObj, {}, key);
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
/**
|
|
84
|
-
export function createTheme<T extends Record<string, unknown>>(themeObj: T) {
|
|
85
|
-
const result = createScopedTheme(themeObj);
|
|
89
|
+
/** 全局主题,`key` 同 `createScopedTheme` */
|
|
90
|
+
export function createTheme<T extends Record<string, unknown>>(themeObj: T, key?: string) {
|
|
91
|
+
const result = createScopedTheme(themeObj, key);
|
|
86
92
|
document.adoptedStyleSheets.push(result[SheetToken].getStyle());
|
|
87
93
|
return result;
|
|
88
94
|
}
|
|
@@ -94,6 +100,7 @@ export function createOverrideTheme<T extends Record<string, unknown>>(theme: Th
|
|
|
94
100
|
|
|
95
101
|
/**
|
|
96
102
|
* 返回一个主题对象,但同时又是一个装饰器函数,用来装饰一个主题更新函数
|
|
103
|
+
* 所有实例共享变量名,`key` 同 `createScopedTheme`
|
|
97
104
|
*
|
|
98
105
|
* @example
|
|
99
106
|
* ```ts
|
|
@@ -104,8 +111,8 @@ export function createOverrideTheme<T extends Record<string, unknown>>(theme: Th
|
|
|
104
111
|
* }
|
|
105
112
|
* ```
|
|
106
113
|
* */
|
|
107
|
-
export function createDecoratorTheme<T extends Record<string, unknown>>(themeObj: T) {
|
|
108
|
-
const themeAsKeys = createThemeFromProps(themeObj);
|
|
114
|
+
export function createDecoratorTheme<T extends Record<string, unknown>>(themeObj: T, key?: string) {
|
|
115
|
+
const themeAsKeys = createThemeFromProps(themeObj, {}, key);
|
|
109
116
|
const props = getThemeProps(themeAsKeys);
|
|
110
117
|
const update = <E extends GemElement, V extends () => T, K = any[] | undefined>(
|
|
111
118
|
getDep?: K extends readonly any[] ? (instance: E) => K : undefined,
|
package/src/lib/decorators.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { Store } from './store';
|
|
|
7
7
|
import { camelToKebabCase, GemError, PropProxyMap } from './utils';
|
|
8
8
|
|
|
9
9
|
type GemElementPrototype = GemElement & { '': never };
|
|
10
|
-
type StaticField = Exclude<keyof Metadata, keyof ShadowRootInit | 'aria' | 'noBlocking' | 'penetrable'>;
|
|
10
|
+
type StaticField = Exclude<keyof Metadata, keyof ShadowRootInit | 'aria' | 'noBlocking' | 'penetrable' | 'attrProps'>;
|
|
11
11
|
|
|
12
12
|
const { deleteProperty, getOwnPropertyDescriptor, defineProperty } = Reflect;
|
|
13
13
|
const { getPrototypeOf, assign, hasOwn } = Object;
|
|
@@ -32,12 +32,31 @@ function clearField<T extends GemElement>(instance: T, prop: string) {
|
|
|
32
32
|
(instance as any)[prop] = value;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* 定义响应式的成员:在原型上定义访问器(基类定义过就跳过),移除实例上的同名字段,
|
|
37
|
+
* `field` 记录到元数据中,供外部读取
|
|
38
|
+
*/
|
|
39
|
+
function defineMember(
|
|
40
|
+
context: ClassFieldDecoratorContext,
|
|
41
|
+
field: StaticField,
|
|
42
|
+
name: string,
|
|
43
|
+
define: (target: GemElementPrototype, prop: string) => void,
|
|
44
|
+
) {
|
|
45
|
+
const prop = context.name as string;
|
|
46
|
+
pushStaticField(context, field, name);
|
|
47
|
+
context.addInitializer(function (this: GemElement) {
|
|
48
|
+
const target = getDecoratorTarget(this, context);
|
|
49
|
+
if (!hasOwn(target, prop)) define(target, prop);
|
|
50
|
+
clearField(this, prop);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
36
54
|
// hack 修改 attribute 行为,如果是观察的,就使用 `setter`
|
|
37
55
|
// 不在 Devtools 中工作 https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#dom_access
|
|
38
56
|
// 使用 Gem DevTools 注入脚本监听 attribute 变化
|
|
39
57
|
const { setAttribute, toggleAttribute, removeAttribute } = Element.prototype;
|
|
40
|
-
const getObservedPropName = (i: GemElement, n: string) =>
|
|
58
|
+
const getObservedPropName = (i: GemElement, n: string) =>
|
|
59
|
+
((i.constructor as any)[Symbol.metadata] as Metadata | undefined)?.attrProps?.[n];
|
|
41
60
|
GemElement.prototype.setAttribute = function (n: string, v: string) {
|
|
42
61
|
const prop = getObservedPropName(this, n);
|
|
43
62
|
if (!prop) return setAttribute.call(this, n, v);
|
|
@@ -130,19 +149,12 @@ type AttrType = BooleanConstructor | NumberConstructor | StringConstructor;
|
|
|
130
149
|
function decoratorAttr<T extends GemElement>(context: ClassFieldDecoratorContext<T>, attrType: AttrType) {
|
|
131
150
|
const prop = context.name as string;
|
|
132
151
|
const attr = camelToKebabCase(prop);
|
|
133
|
-
context.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
clearField(this, prop);
|
|
140
|
-
// 记录观察的 attribute, 用于 hack 的 setAttribute
|
|
141
|
-
const proto = getPrototypeOf(this);
|
|
142
|
-
const attrMap = observedTargetAttributes.get(proto) || new Map<string, string>();
|
|
143
|
-
attrMap.set(attr, prop);
|
|
144
|
-
observedTargetAttributes.set(proto, attrMap);
|
|
145
|
-
});
|
|
152
|
+
const metadata = context.metadata as Metadata;
|
|
153
|
+
// 记录观察的 attribute,用于 hack 的 setAttribute,原型链继承基类的记录
|
|
154
|
+
if (!hasOwn(metadata, 'attrProps')) metadata.attrProps = Object.create(metadata.attrProps || null);
|
|
155
|
+
metadata.attrProps![attr] = prop;
|
|
156
|
+
// `observedAttributes` 没有 observe 的效果
|
|
157
|
+
defineMember(context, 'observedAttributes', attr, (target) => defineProp(target, prop, { attr, attrType }));
|
|
146
158
|
// 延时定义的元素需要继承原实例属性值
|
|
147
159
|
return function (this: any, initValue: any) {
|
|
148
160
|
return this.getAttribute(attr) ?? initValue;
|
|
@@ -181,14 +193,7 @@ export function numattribute<T extends GemElement>(_: undefined, context: ClassF
|
|
|
181
193
|
*/
|
|
182
194
|
export function property<T extends GemElement>(_: undefined, context: ClassFieldDecoratorContext<T>) {
|
|
183
195
|
const prop = context.name as string;
|
|
184
|
-
context
|
|
185
|
-
const target = getDecoratorTarget(this, context);
|
|
186
|
-
if (!hasOwn(target, prop)) {
|
|
187
|
-
pushStaticField(context, 'observedProperties', prop);
|
|
188
|
-
defineProp(target, prop);
|
|
189
|
-
}
|
|
190
|
-
clearField(this, prop);
|
|
191
|
-
});
|
|
196
|
+
defineMember(context, 'observedProperties', prop, defineProp);
|
|
192
197
|
// 延时定义的元素需要继承原实例属性值
|
|
193
198
|
return function (this: any, initValue: any) {
|
|
194
199
|
return this[prop] ?? initValue;
|
|
@@ -342,16 +347,8 @@ function defineCSSState(target: GemElementPrototype, prop: string, stateStr: str
|
|
|
342
347
|
* ```
|
|
343
348
|
*/
|
|
344
349
|
export function state<T extends GemElement>(_: undefined, context: ClassFieldDecoratorContext<T, boolean>) {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
context.addInitializer(function (this: T) {
|
|
348
|
-
const target = getDecoratorTarget(this, context);
|
|
349
|
-
if (!hasOwn(target, prop)) {
|
|
350
|
-
pushStaticField(context, 'definedCSSStates', attr);
|
|
351
|
-
defineCSSState(target, prop, attr);
|
|
352
|
-
}
|
|
353
|
-
clearField(this, prop);
|
|
354
|
-
});
|
|
350
|
+
const stateStr = camelToKebabCase(context.name as string);
|
|
351
|
+
defineMember(context, 'definedCSSStates', stateStr, (target, prop) => defineCSSState(target, prop, stateStr));
|
|
355
352
|
}
|
|
356
353
|
|
|
357
354
|
function defineStaticField(
|
|
@@ -421,7 +418,8 @@ export type Emitter<T = any> = <_Listener = EmitterListener<T>>(
|
|
|
421
418
|
|
|
422
419
|
/**
|
|
423
420
|
* 定义一个事件发射器,类似 `HTMLElement.click`,
|
|
424
|
-
*
|
|
421
|
+
* 调用时将在微任务中发送一个和字段名称的烤串式格式的自定义事件,
|
|
422
|
+
* 避免同步触发时祖先元素还没挂载好(https://github.com/mantou132/gem/issues/203)
|
|
425
423
|
*
|
|
426
424
|
* @example
|
|
427
425
|
* ```ts
|
|
@@ -436,20 +434,9 @@ export function emitter<T extends GemElement>(_: undefined, context: ClassFieldD
|
|
|
436
434
|
export function globalemitter<T extends GemElement>(_: undefined, context: ClassFieldDecoratorContext<T, Emitter>) {
|
|
437
435
|
defineEmitter(context, { bubbles: true, composed: true });
|
|
438
436
|
}
|
|
439
|
-
function defineEmitter<
|
|
440
|
-
context
|
|
441
|
-
|
|
442
|
-
) {
|
|
443
|
-
const prop = context.name as string;
|
|
444
|
-
const event = camelToKebabCase(prop);
|
|
445
|
-
context.addInitializer(function (this: T) {
|
|
446
|
-
const target = getDecoratorTarget(this, context);
|
|
447
|
-
if (!hasOwn(target, prop)) {
|
|
448
|
-
pushStaticField(context, 'definedEvents', event);
|
|
449
|
-
defineProp(target, prop, { event, eventOptions });
|
|
450
|
-
}
|
|
451
|
-
clearField(this, prop);
|
|
452
|
-
});
|
|
437
|
+
function defineEmitter(context: ClassFieldDecoratorContext, eventOptions?: Omit<CustomEventInit<unknown>, 'detail'>) {
|
|
438
|
+
const event = camelToKebabCase(context.name as string);
|
|
439
|
+
defineMember(context, 'definedEvents', event, (target, prop) => defineProp(target, prop, { event, eventOptions }));
|
|
453
440
|
}
|
|
454
441
|
|
|
455
442
|
/**
|