@mantou/gem 2.2.13 → 2.2.15

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.
Files changed (73) hide show
  1. package/dist/gem.js +7 -7
  2. package/dist/gem.mjs +7 -7
  3. package/elements/base/gesture.d.ts +7 -1
  4. package/elements/base/gesture.d.ts.map +1 -1
  5. package/elements/base/gesture.js +23 -15
  6. package/elements/base/gesture.js.map +1 -1
  7. package/elements/base/link.d.ts.map +1 -1
  8. package/elements/base/link.js +6 -3
  9. package/elements/base/link.js.map +1 -1
  10. package/elements/base/modal-factory.d.ts +1 -1
  11. package/elements/base/modal-factory.d.ts.map +1 -1
  12. package/elements/base/use.d.ts.map +1 -1
  13. package/elements/base/use.js +11 -0
  14. package/elements/base/use.js.map +1 -1
  15. package/helper/hmr.d.ts +3 -2
  16. package/helper/hmr.d.ts.map +1 -1
  17. package/helper/hmr.js +95 -23
  18. package/helper/hmr.js.map +1 -1
  19. package/helper/i18n.d.ts +2 -1
  20. package/helper/i18n.d.ts.map +1 -1
  21. package/helper/i18n.js +9 -1
  22. package/helper/i18n.js.map +1 -1
  23. package/helper/theme.d.ts.map +1 -1
  24. package/helper/theme.js +5 -0
  25. package/helper/theme.js.map +1 -1
  26. package/lib/decorators.d.ts +9 -1
  27. package/lib/decorators.d.ts.map +1 -1
  28. package/lib/decorators.js +4 -3
  29. package/lib/decorators.js.map +1 -1
  30. package/lib/history.d.ts +10 -7
  31. package/lib/history.d.ts.map +1 -1
  32. package/lib/history.js +20 -16
  33. package/lib/history.js.map +1 -1
  34. package/lib/lit-html.d.ts +15 -0
  35. package/lib/lit-html.d.ts.map +1 -1
  36. package/lib/lit-html.js +3 -1
  37. package/lib/lit-html.js.map +1 -1
  38. package/lib/reactive.d.ts +4 -1
  39. package/lib/reactive.d.ts.map +1 -1
  40. package/lib/reactive.js +41 -18
  41. package/lib/reactive.js.map +1 -1
  42. package/lib/server.d.ts +14 -0
  43. package/lib/server.d.ts.map +1 -0
  44. package/lib/server.js +43 -0
  45. package/lib/server.js.map +1 -0
  46. package/lib/store.d.ts +10 -1
  47. package/lib/store.d.ts.map +1 -1
  48. package/lib/store.js +36 -9
  49. package/lib/store.js.map +1 -1
  50. package/lib/utils.d.ts +2 -2
  51. package/lib/utils.d.ts.map +1 -1
  52. package/lib/utils.js +20 -7
  53. package/lib/utils.js.map +1 -1
  54. package/lib/version.d.ts +1 -1
  55. package/lib/version.js +1 -1
  56. package/package.json +1 -1
  57. package/src/elements/base/gesture.ts +26 -15
  58. package/src/elements/base/link.ts +5 -3
  59. package/src/elements/base/use.ts +11 -0
  60. package/src/helper/hmr.ts +105 -37
  61. package/src/helper/i18n.ts +12 -1
  62. package/src/helper/theme.ts +4 -0
  63. package/src/lib/decorators.ts +10 -10
  64. package/src/lib/history.ts +20 -16
  65. package/src/lib/lit-html.ts +3 -1
  66. package/src/lib/reactive.ts +40 -18
  67. package/src/lib/server.ts +45 -0
  68. package/src/lib/store.ts +40 -9
  69. package/src/lib/utils.ts +22 -7
  70. package/src/lib/version.ts +1 -1
  71. package/src/test/elements/gesture.test.ts +26 -1
  72. package/src/test/gem-element/basic.test.ts +20 -0
  73. package/src/test/store.test.ts +12 -0
@@ -42,6 +42,17 @@ const SWIPE_MIN_SPEED = 0.3; // px/ms
42
42
  const SWIPE_MIN_DISTANCE = 20;
43
43
  const SWIPE_DIRECTION_SLOP = 10;
44
44
 
45
+ /** 移动超过该距离后锁定本次触摸的方向,与 iOS 一样同一次触摸只交给一个方向的手势。 */
46
+ export const AXIS_LOCK_SLOP = 10;
47
+ /** 判定偏向纵向,滚动、下拉不会因为拇指的横向抖动被抢走。 */
48
+ export const AXIS_LOCK_BIAS = 1.5;
49
+
50
+ /** 根据相对起点的位移锁定方向,位移不足时返回 `undefined`。 */
51
+ export function lockAxis(dx: number, dy: number): 'x' | 'y' | undefined {
52
+ if (Math.max(Math.abs(dx), Math.abs(dy)) < AXIS_LOCK_SLOP) return;
53
+ return Math.abs(dx) > Math.abs(dy) * AXIS_LOCK_BIAS ? 'x' : 'y';
54
+ }
55
+
45
56
  /** Viewport coordinates in CSS pixels and a monotonic timestamp in milliseconds. */
46
57
  export interface GestureSample {
47
58
  clientX: number;
@@ -54,8 +65,8 @@ export function getSwipe(
54
65
  startEvent: GestureSample,
55
66
  moves: readonly GestureSample[],
56
67
  evt: GestureSample,
57
- getMovementX?: (x: number, y: number) => number,
58
- getMovementY?: (x: number, y: number) => number,
68
+ getMovementX?: (x: number) => number,
69
+ getMovementY?: (y: number) => number,
59
70
  ): SwipeEventDetail | undefined {
60
71
  const targetTime = Math.max(startEvent.timeStamp, evt.timeStamp - SWIPE_TIME_WINDOW);
61
72
  let sample: GestureSample = evt;
@@ -98,7 +109,7 @@ export function getSwipe(
98
109
  const dy = evt.clientY - sample.clientY;
99
110
  const horizontal = Math.abs(dx) > Math.abs(dy);
100
111
  if (Math.abs(dx) === Math.abs(dy)) return;
101
- const movement = horizontal ? (getMovementX?.(dx, dy) ?? dx) : (getMovementY?.(dx, dy) ?? dy);
112
+ const movement = horizontal ? (getMovementX?.(dx) ?? dx) : (getMovementY?.(dy) ?? dy);
102
113
  const distance = Math.abs(movement);
103
114
  const speed = distance / duration;
104
115
  if (distance < SWIPE_MIN_DISTANCE || speed < SWIPE_MIN_SPEED) return;
@@ -151,6 +162,7 @@ export class GemGestureElement extends GemElement {
151
162
 
152
163
  #pressed = false; // 触发 press 之后不触发其他事件
153
164
  #gestureTriggered = false; // 会排除 touchAction 方向
165
+ #axis?: 'x' | 'y'; // 设置了 touchAction 时,本次单指触摸锁定的方向
154
166
  #multiTouch = false;
155
167
  #pressTimer: ReturnType<typeof setTimeout> | number = 0;
156
168
 
@@ -173,32 +185,27 @@ export class GemGestureElement extends GemElement {
173
185
  }
174
186
  };
175
187
 
176
- #getMovementX = (x: number, y: number) => {
188
+ // 交给浏览器的方向,以及方向锁定之前和锁定之外的位移都不属于本元素
189
+ #getMovementX = (x: number) => {
177
190
  const touchAction = this.touchAction;
178
191
  if (
179
192
  (touchAction.includes('pan-right') && x > 0) ||
180
193
  (touchAction.includes('pan-left') && x < 0) ||
181
194
  touchAction.includes('pan-x') ||
182
- (((touchAction.includes('pan-down') && y > 0) ||
183
- (touchAction.includes('pan-up') && y < 0) ||
184
- touchAction.includes('pan-y')) &&
185
- Math.abs(y) > Math.abs(x)) // horizontally scrolling
195
+ (touchAction.includes('pan-') && this.#axis !== 'x')
186
196
  ) {
187
197
  return 0;
188
198
  }
189
199
  return x;
190
200
  };
191
201
 
192
- #getMovementY = (x: number, y: number) => {
202
+ #getMovementY = (y: number) => {
193
203
  const touchAction = this.touchAction;
194
204
  if (
195
205
  (touchAction.includes('pan-down') && y > 0) ||
196
206
  (touchAction.includes('pan-up') && y < 0) ||
197
207
  touchAction.includes('pan-y') ||
198
- (((touchAction.includes('pan-right') && x > 0) ||
199
- (touchAction.includes('pan-left') && x < 0) ||
200
- touchAction.includes('pan-x')) &&
201
- Math.abs(x) > Math.abs(y)) // vertical scrolling
208
+ (touchAction.includes('pan-') && this.#axis !== 'y')
202
209
  ) {
203
210
  return 0;
204
211
  }
@@ -215,6 +222,7 @@ export class GemGestureElement extends GemElement {
215
222
  if (evt.isPrimary) {
216
223
  this.#pressed = false;
217
224
  this.#gestureTriggered = false;
225
+ this.#axis = undefined;
218
226
  this.#pressTimer = setTimeout(() => {
219
227
  this.press(evt);
220
228
  this.#pressed = true;
@@ -236,9 +244,12 @@ export class GemGestureElement extends GemElement {
236
244
  // https://bugs.chromium.org/p/chromium/issues/detail?id=1092358
237
245
  const movementX = clientX - lastMove.clientX;
238
246
  const movementY = clientY - lastMove.clientY;
247
+ if (!this.#axis && this.#startEventMap.size === 1) {
248
+ this.#axis = lockAxis(clientX - startEvent.clientX, clientY - startEvent.clientY);
249
+ }
239
250
  const move = {
240
- x: this.#getMovementX(movementX, movementY),
241
- y: this.#getMovementY(movementX, movementY),
251
+ x: this.#getMovementX(movementX),
252
+ y: this.#getMovementY(movementY),
242
253
  clientX,
243
254
  clientY,
244
255
  timeStamp,
@@ -14,6 +14,7 @@ import {
14
14
  } from '../../lib/decorators';
15
15
  import { css, GemElement, html } from '../../lib/element';
16
16
  import { basePathStore, history } from '../../lib/history';
17
+ import { isServer } from '../../lib/server';
17
18
  import { absoluteLocation } from '../../lib/utils';
18
19
  import type { RouteItem, RouteOptions } from './route';
19
20
  import { createHistoryParams, createPath, matchPath } from './route';
@@ -125,9 +126,10 @@ export class GemLinkElement extends GemElement {
125
126
 
126
127
  #getHint = () => {
127
128
  const locationString = this._getLocationString();
128
- return isExternal(locationString)
129
- ? locationString
130
- : new URL(history.basePath + locationString, location.origin).toString();
129
+ if (isExternal(locationString)) return locationString;
130
+ const url = new URL(history.basePath + locationString, location.origin);
131
+ // 客户端输出完整 URL 便于打印,服务端(尤其是预渲染)的 origin 不可靠,使用根相对路径
132
+ return isServer ? url.pathname + url.search + url.hash : url.toString();
131
133
  };
132
134
 
133
135
  #preload = () => {
@@ -1,5 +1,6 @@
1
1
  import { adoptedStyle, attribute, property, shadow, state, template } from '../../lib/decorators';
2
2
  import { css, GemElement, html } from '../../lib/reactive';
3
+ import { isServer } from '../../lib/server';
3
4
 
4
5
  const styles = css`
5
6
  :host(:where(:not([hidden]))) {
@@ -14,6 +15,7 @@ const styles = css`
14
15
  `;
15
16
 
16
17
  const eleCache = new Map<string, HTMLTemplateElement>();
18
+ const stringsCache = new Map<string, TemplateStringsArray>();
17
19
  /**
18
20
  * svg 中的 `<use>` 不能穿透 ShadowDOM,
19
21
  * 此元素用来模拟 `<use>`,
@@ -31,6 +33,15 @@ export class GemUseElement extends GemElement {
31
33
 
32
34
  #getContent = () => {
33
35
  const ele = this.element || (this.selector ? (this.root || document).querySelector(this.selector) : null);
36
+ // SSR 没有 DOM,把字符串作为静态模版输出
37
+ if (typeof ele === 'string' && isServer) {
38
+ let strings = stringsCache.get(ele);
39
+ if (!strings) {
40
+ strings = Object.assign([ele], { raw: [ele] });
41
+ stringsCache.set(ele, strings);
42
+ }
43
+ return html(strings);
44
+ }
34
45
  if (typeof ele === 'string') {
35
46
  let temp = eleCache.get(ele);
36
47
  if (!temp) {
package/src/helper/hmr.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { attribute, boolattribute, emitter, globalemitter, numattribute, property, state } from '../lib/decorators';
2
+ import { ChildPart, type TemplateResult } from '../lib/lit-html';
2
3
  import { GemElement, type Metadata, UpdateToken } from '../lib/reactive';
3
4
  import type { Store } from '../lib/store';
5
+ import { camelToKebabCase } from '../lib/utils';
4
6
  import { Logger } from './logger';
5
7
 
6
8
  const logger = new Logger('HMR');
@@ -41,37 +43,23 @@ function getHmrMethodKeys(obj: any) {
41
43
  return Object.getOwnPropertyNames(obj).filter((key) => key.startsWith('_hmr_'));
42
44
  }
43
45
 
44
- /** 不支持修改 store */
46
+ /** 连接的 store 必须是同一对象,否则元素订阅的是老 store */
45
47
  function checkMetadataNeedReload({ mode, penetrable, noBlocking, observedStores }: Metadata, newMetadata: Metadata) {
46
- const stringifyStores = (stores?: Store<any>[]) =>
47
- JSON.stringify(
48
- stores?.map((store) => JSON.stringify({ ...store }, (_, v) => (typeof v === 'function' ? v.toString() : v))),
49
- );
48
+ const isSameStores = (a: Store<any>[] = [], b: Store<any>[] = []) =>
49
+ a.length === b.length && a.every((store, i) => store === b[i]);
50
50
  if (
51
51
  mode !== newMetadata.mode ||
52
52
  penetrable !== newMetadata.penetrable ||
53
53
  noBlocking !== newMetadata.noBlocking ||
54
- stringifyStores(observedStores) !== stringifyStores(newMetadata.observedStores)
54
+ !isSameStores(observedStores, newMetadata.observedStores)
55
55
  ) {
56
56
  return true;
57
57
  }
58
58
  }
59
59
 
60
- /** 不支持删减函数字段,因为原函数可能已经被绑定 */
61
- function checkClassNeedReload(existed: CustomElementConstructor, newClass: CustomElementConstructor) {
62
- if (
63
- getHmrMethodKeys(existed).join() !== getHmrMethodKeys(newClass).join() ||
64
- getHmrMethodKeys(existed.prototype).join() !== getHmrMethodKeys(newClass.prototype).join()
65
- ) {
66
- return true;
67
- }
68
- }
69
-
70
- /** 不支持删减实例其他字段,因为新实例总是使用老 Class 定义 */
60
+ /** 不支持删减实例其他字段、有装饰器的函数成员和构造函数,因为新实例总是使用老 Class 定义 */
71
61
  function checkFieldsNeedReload({ instanceFields }: ReturnType<typeof getFields>) {
72
- if (instanceFields.other?.add?.length || instanceFields.other?.remove?.length) {
73
- return true;
74
- }
62
+ return [instanceFields.other, instanceFields.ctor].some((diff) => diff?.add.length || diff?.remove.length);
75
63
  }
76
64
 
77
65
  function diffArr<T>(oldList: T[], newList: T[], fn: (i: T) => string = (e) => String(e)) {
@@ -98,10 +86,15 @@ function diffRecord<T>(oldRecord: Partial<Record<string, T[]>>, newRecord: Parti
98
86
  return diffArr(oldKeys, newKeys);
99
87
  }
100
88
 
101
- type HasFieldsRecordClass = { _defined_fields_?: [string, string, boolean][] };
89
+ type HasFieldsRecordClass = { _defined_fields_?: [string, string, boolean, string?][] };
102
90
  function getFields(existed: CustomElementConstructor, newClass: CustomElementConstructor) {
103
91
  const translate = (cls: HasFieldsRecordClass) => {
104
- const fieldList = (cls._defined_fields_ || []).map(([name, type, isStatic]) => ({ name, type, isStatic }));
92
+ const fieldList = (cls._defined_fields_ || []).map(([name, type, isStatic, shadow]) => ({
93
+ name,
94
+ type,
95
+ isStatic,
96
+ shadow,
97
+ }));
105
98
  const fieldGroup = Object.groupBy(fieldList, ({ isStatic }) => (isStatic ? 'staticFields' : 'instanceFields'));
106
99
  const groupBy = (list: typeof fieldGroup.staticFields) => Object.groupBy(list || [], ({ type }) => type);
107
100
  return {
@@ -146,6 +139,44 @@ const descMap = {
146
139
  globalemitter,
147
140
  };
148
141
 
142
+ const attrTypes = new Set(['attribute', 'boolattribute', 'numattribute']);
143
+
144
+ /** 新增 attribute 的初始值本应在构造时读取,但新实例仍由老类构造,改为首次读取时从 DOM 获取 */
145
+ function lazyInitAttribute(proto: any, prop: string) {
146
+ const { get, set } = Object.getOwnPropertyDescriptor(proto, prop)!;
147
+ const attr = camelToKebabCase(prop);
148
+ Reflect.defineProperty(proto, prop, {
149
+ configurable: true,
150
+ set,
151
+ get() {
152
+ if (get!.call(this) === undefined) set!.call(this, this.getAttribute(attr));
153
+ return get!.call(this);
154
+ },
155
+ });
156
+ }
157
+
158
+ /** 新增的箭头函数字段不会被老类构造,首次访问时为实例(静态字段为类)创建代理 */
159
+ function defineLazyFn(target: any, name: string, shadow: string) {
160
+ Reflect.defineProperty(target, name, {
161
+ configurable: true,
162
+ get() {
163
+ const fn = (...args: any[]) => this[shadow](...args);
164
+ setProperty(this, name, fn);
165
+ return fn;
166
+ },
167
+ });
168
+ }
169
+
170
+ /** 函数成员的代理只调用影子方法,可以直接复制到老类上 */
171
+ function updateFnMembers(oldTarget: any, newTarget: any, diffs: ReturnType<typeof getFields>['staticFields']) {
172
+ const { method, fn } = diffs;
173
+ method?.add.concat(method.some).forEach(({ name }) => {
174
+ Reflect.defineProperty(oldTarget, name, Object.getOwnPropertyDescriptor(newTarget, name)!);
175
+ });
176
+ fn?.add.forEach(({ name, shadow }) => defineLazyFn(oldTarget, name, shadow!));
177
+ [method?.remove, fn?.remove].forEach((list) => list?.forEach(({ name }) => deleteProperty(oldTarget, name)));
178
+ }
179
+
149
180
  function setArrValue<T>(obj: any, field: string, val: T[] = []) {
150
181
  if (!Object.getOwnPropertyDescriptor(obj, field)) {
151
182
  setProperty(obj, field, val);
@@ -159,18 +190,43 @@ function setArrValue<T>(obj: any, field: string, val: T[] = []) {
159
190
  declare global {
160
191
  interface Window {
161
192
  _hmrClassRegistry: Map<string, any>;
162
- _hmrRegisterClass: (name: string) => (cls: HasFieldsRecordClass, ctx: ClassDecoratorContext) => void;
193
+ _hmrRegisterClass: (name: string) => (cls: HasFieldsRecordClass, ctx: ClassDecoratorContext) => any;
194
+ _hmrKeep: <T>(key: string, fn: () => T) => T;
163
195
  }
164
196
  }
165
197
 
166
198
  if (typeof window !== 'undefined' && nativeDefineElement && !window._hmrClassRegistry) {
167
199
  window._hmrClassRegistry = new Map();
200
+
201
+ // 模块重新执行时复用模块顶层的 store
202
+ const keptValues = new Map<string, any>();
203
+ window._hmrKeep = (key, fn) => {
204
+ if (!keptValues.has(key)) keptValues.set(key, fn());
205
+ return keptValues.get(key);
206
+ };
207
+
208
+ // 模块重新执行后模板字符串是新对象,内容相同时使用老对象,以便复用已渲染的 DOM
209
+ const canonicalStrings = new Map<string, TemplateStringsArray>();
210
+ const stringsMap = new WeakMap<TemplateStringsArray, TemplateStringsArray>();
211
+ const { _$getTemplate } = ChildPart.prototype;
212
+ ChildPart.prototype._$getTemplate = function (result: TemplateResult) {
213
+ let strings = stringsMap.get(result.strings);
214
+ if (!strings) {
215
+ const key = `${result.type}:${result.strings.join('\0')}`;
216
+ strings = canonicalStrings.get(key) || result.strings;
217
+ canonicalStrings.set(key, strings);
218
+ stringsMap.set(result.strings, strings);
219
+ }
220
+ result.strings = strings;
221
+ return _$getTemplate.call(this, result);
222
+ };
223
+
168
224
  window._hmrRegisterClass =
169
225
  (name: string) =>
170
226
  (cls: HasFieldsRecordClass, { addInitializer }: ClassDecoratorContext) => {
171
- addInitializer(() => {
172
- const existed = window._hmrClassRegistry.get(name);
227
+ const existed = window._hmrClassRegistry.get(name);
173
228
 
229
+ addInitializer(() => {
174
230
  if (!existed) {
175
231
  window._hmrClassRegistry.set(name, cls);
176
232
  return;
@@ -182,7 +238,7 @@ if (typeof window !== 'undefined' && nativeDefineElement && !window._hmrClassReg
182
238
  while (current) {
183
239
  const [oldClass, newClass] = current;
184
240
  if (oldClass === newClass || current.some((e) => e === GemElement || e === Function.prototype)) break;
185
- if (current.some((e) => !e) || checkClassNeedReload(oldClass, newClass)) {
241
+ if (current.some((e) => !e)) {
186
242
  location.reload();
187
243
  return;
188
244
  }
@@ -246,10 +302,17 @@ if (typeof window !== 'undefined' && nativeDefineElement && !window._hmrClassReg
246
302
  }
247
303
  });
248
304
  remove.forEach((prop) => {
249
- setProperty(element, prop.name, undefined);
305
+ if (type in descMap) {
306
+ setProperty(element, prop.name, undefined);
307
+ }
250
308
  });
251
309
  });
252
310
  });
311
+ instanceFieldsEntries.forEach(([type, { add }]) => {
312
+ if (attrTypes.has(type)) add.forEach((prop) => lazyInitAttribute(oldClass.prototype, prop.name));
313
+ });
314
+ updateFnMembers(oldClass, newClass, fields.staticFields);
315
+ updateFnMembers(oldClass.prototype, newClass.prototype, fields.instanceFields);
253
316
  // 删除的属性在 proto 上添加占位符
254
317
  instanceFieldsEntries.forEach(([type, { remove }]) => {
255
318
  remove.forEach((prop) => {
@@ -269,23 +332,25 @@ if (typeof window !== 'undefined' && nativeDefineElement && !window._hmrClassReg
269
332
  setArrValue(oldMetadata, 'adoptedStyleSheets', newMetadata.adoptedStyleSheets);
270
333
  setArrValue(oldClass, '_defined_fields_', newClass._defined_fields_);
271
334
 
335
+ // 全部替换,让所有方法都引用新模块的变量
272
336
  [
273
337
  [oldClass, newClass],
274
338
  [oldClass.prototype, newClass.prototype],
275
339
  ].forEach(([existedObj, newObj]) => {
276
- Object.assign(
277
- existedObj,
278
- Object.fromEntries(
279
- getHmrMethodKeys(newObj)
280
- .filter((key) => existedObj[key].toString() !== newObj[key].toString())
281
- .map((key) => [key, newObj[key]]),
282
- ),
283
- );
340
+ getHmrMethodKeys(newObj).forEach((key) => (existedObj[key] = newObj[key]));
284
341
  });
285
342
 
286
343
  current = [getPrototypeOf(oldClass), getPrototypeOf(newClass)];
287
344
  }
288
345
  });
346
+
347
+ if (!existed) return;
348
+
349
+ // 新类未注册,构造时使用老类,例如 `new MyElement()`
350
+ const proxy: any = new Proxy(cls, {
351
+ construct: (_, args, newTarget) => Reflect.construct(existed, args, newTarget === proxy ? existed : newTarget),
352
+ });
353
+ return proxy;
289
354
  };
290
355
 
291
356
  window.customElements.define = (name: string, cls: CustomElementConstructor) => {
@@ -301,8 +366,11 @@ if (typeof window !== 'undefined' && nativeDefineElement && !window._hmrClassReg
301
366
  logger.info(`<${name}> update,`, { cls });
302
367
 
303
368
  updateElement(name, (element) => {
304
- // 触发样式更新,支持 Light DOM
305
- element.after(element);
369
+ // 触发样式更新,支持 Light DOM;不移动元素,避免失去焦点和滚动位置
370
+ if (element.isConnected) {
371
+ (element as GemElement).disconnectedCallback();
372
+ (element as GemElement).connectedCallback();
373
+ }
306
374
  // 重新渲染
307
375
  (element as any)[UpdateToken]?.();
308
376
  });
@@ -2,6 +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
6
  import { GemError } from '../lib/utils';
6
7
  import { logger } from './logger';
7
8
 
@@ -65,13 +66,23 @@ interface I18nOptions<T> {
65
66
  export class I18n<T = Record<string, Msg>> implements RouteTrigger {
66
67
  resources: Resources<T>;
67
68
  fallbackLanguage: string;
68
- currentLanguage: string;
69
69
  cache?: boolean | 'manual';
70
70
  cachePrefix = 'gem-i18n';
71
71
  urlParamsType?: 'path' | 'querystring' | 'ccTLD' | 'gTLD';
72
72
  urlParamsName?: string;
73
73
  onChange?: (currentLanguage: string) => void;
74
74
 
75
+ // SSR 时每个请求使用独立的语言
76
+ #state = isolate({ currentLanguage: '' });
77
+
78
+ get currentLanguage() {
79
+ return this.#state.currentLanguage;
80
+ }
81
+
82
+ set currentLanguage(lang: string) {
83
+ this.#state.currentLanguage = lang;
84
+ }
85
+
75
86
  // 兼容 `<gem-route>`
76
87
  replace = ({ path }: UpdateHistoryParams) => this.setLanguage(path!);
77
88
  getParams = () => ({ path: this.currentLanguage });
@@ -1,5 +1,6 @@
1
1
  import type { GemElement, Sheet } from '../lib/reactive';
2
2
  import { css, SheetToken } from '../lib/reactive';
3
+ import { isServer } from '../lib/server';
3
4
  import type { Store } from '../lib/store';
4
5
  import { connect, createStore } from '../lib/store';
5
6
  import type { Updater } from '../lib/utils';
@@ -117,6 +118,9 @@ export function createDecoratorTheme<T extends Record<string, unknown>>(themeObj
117
118
  this.internals.sheets.push(style);
118
119
  this.memo(
119
120
  () => {
121
+ // https://bugs.webkit.org/show_bug.cgi?id=326327
122
+ // SSR 需要更新样式表文本
123
+ if (isServer || CSS.supports(`font:-apple-system-body`)) return theme(access.get(this).apply(this));
120
124
  const next = access.get(this).apply(this);
121
125
  const rule = style.cssRules[0] as CSSStyleRule;
122
126
  const eleTarget = this.shadowRoot
@@ -2,7 +2,7 @@
2
2
 
3
3
  import type { TemplateResult } from './lit-html';
4
4
  import type { Metadata, Sheet } from './reactive';
5
- import { _createTemplate, _RenderErrorEvent, GemElement, render, UpdateToken } from './reactive';
5
+ import { _createTemplate, _RenderErrorEvent, _renderRoot, GemElement, UpdateToken } from './reactive';
6
6
  import type { Store } from './store';
7
7
  import { camelToKebabCase, GemError, PropProxyMap } from './utils';
8
8
 
@@ -300,8 +300,9 @@ export function template<T extends GemElement, V extends () => TemplateResult |
300
300
  export function fallback<T extends GemElement, V extends (err: any) => TemplateResult | null | undefined>() {
301
301
  return (_: any, { addInitializer, access }: ClassFieldDecoratorContext<T, V> | ClassMethodDecoratorContext<T, V>) => {
302
302
  addInitializer(function (this: T) {
303
- this.addEventListener(_RenderErrorEvent, ({ detail }: CustomEvent) => {
304
- render(access.get.call(this, this).apply(this, [detail]), this.internals.shadowRoot || this);
303
+ this.addEventListener(_RenderErrorEvent, (evt: CustomEvent) => {
304
+ evt.preventDefault();
305
+ _renderRoot(access.get.call(this, this).apply(this, [evt.detail]), this.internals.shadowRoot || this);
305
306
  });
306
307
  });
307
308
  };
@@ -408,16 +409,15 @@ export function part(_: undefined, context: ClassFieldDecoratorContext<any, stri
408
409
  }
409
410
 
410
411
  type EmitterHandler<T> = (evt: CustomEvent<T>) => void;
412
+ type EmitterListener<T> = EmitterHandler<T> | (AddEventListenerOptions & { handleEvent: EmitterHandler<T> });
411
413
 
412
- export type Emitter<T = any> = ((
414
+ /**
415
+ * 签名的类型参数 `_Listener` 用来为 ts plugin 提供事件处理函数的类型签名
416
+ */
417
+ export type Emitter<T = any> = <_Listener = EmitterListener<T>>(
413
418
  detail?: T,
414
419
  options?: Omit<CustomEventInit<unknown>, 'detail'>,
415
- ) => Promise<void> | void) & {
416
- /**
417
- * @internal 用来为 ts plugin 提供类型签名,没有值
418
- */
419
- handler?: EmitterHandler<T> | (AddEventListenerOptions & { handleEvent: EmitterHandler<T> });
420
- };
420
+ ) => Promise<void> | void;
421
421
 
422
422
  /**
423
423
  * 定义一个事件发射器,类似 `HTMLElement.click`,
@@ -1,5 +1,6 @@
1
1
  // https://github.com/WICG/navigation-api#stakeholder-feedback
2
2
 
3
+ import { getRequestValue } from './server';
3
4
  import { connect, createStore } from './store';
4
5
  import { absoluteLocation, cleanObject, GemError, QueryString } from './utils';
5
6
 
@@ -45,7 +46,11 @@ type HistoryParams = Omit<UpdateHistoryParams, 'query'> & {
45
46
  hash: string;
46
47
  };
47
48
 
48
- const paramsMap = new Map<string, HistoryParams>();
49
+ const globalParamsMap = new Map<string, HistoryParams>();
50
+ // SSR 时每个请求使用独立的记录,请求结束后释放
51
+ // 从全局复制初始记录(设置 basePath 等操作需要它),记录会被原地修改,所以也要复制
52
+ const copyParamsMap = () => new Map([...globalParamsMap].map(([k, v]) => [k, { ...v }]));
53
+ const getParamsMap = () => getRequestValue(globalParamsMap, copyParamsMap);
49
54
 
50
55
  function validData(data: any) {
51
56
  const { $key, $hasCloseHandle, $hasOpenHandle, $hasShouldCloseHandle } = data || {};
@@ -71,8 +76,7 @@ function dispatchBeforeChangeEvent() {
71
76
 
72
77
  function normalizeParams(params: UpdateHistoryParams): HistoryParams {
73
78
  const current =
74
- paramsMap.get(store.$key) ||
75
- ({ path: getInternalPath(location.pathname), query: new QueryString() } as HistoryParams);
79
+ gemHistory.getParams() || ({ path: getInternalPath(location.pathname), query: new QueryString() } as HistoryParams);
76
80
  // 没提供 path 使用当前 path
77
81
  const path = params.path ? absoluteLocation(current.path, params.path) : getInternalPath(location.pathname);
78
82
  // 没提供 query 又没有提供 path 时使用当前 search
@@ -100,7 +104,7 @@ function updateHistory(p: UpdateHistoryParams, native: typeof nativeHistory.push
100
104
  $title: title,
101
105
  ...data,
102
106
  };
103
- paramsMap.set(state.$key, params);
107
+ getParamsMap().set(state.$key, params);
104
108
  dispatchBeforeChangeEvent();
105
109
  cleanObject(store);
106
110
  store(state);
@@ -120,7 +124,7 @@ function updateHistoryByNative(data: any, title: string, originUrl: string, nati
120
124
  };
121
125
  const { pathname, search, hash } = new URL(originUrl, location.origin + location.pathname);
122
126
  const params = normalizeParams({ path: pathname, query: new QueryString(search), hash, title, data });
123
- paramsMap.set(state.$key, params);
127
+ getParamsMap().set(state.$key, params);
124
128
  dispatchBeforeChangeEvent();
125
129
  cleanObject(store);
126
130
  store(state);
@@ -149,13 +153,13 @@ class GemHistory extends EventTarget {
149
153
  // 应用初始化的时候设置
150
154
  gemBasePathStore({ basePath });
151
155
  // paramsMap 更新后 ui 才会更新
152
- Object.assign(paramsMap.get(store.$key)!, { path: getInternalPath(location.pathname) });
156
+ Object.assign(this.getParams(), { path: getInternalPath(location.pathname) });
153
157
  }
154
158
  getParams() {
155
- return paramsMap.get(store.$key)!;
159
+ return getParamsMap().get(store.$key)!;
156
160
  }
157
161
  updateParams(params: UpdateHistoryParams) {
158
- Object.assign(paramsMap.get(store.$key)!, params);
162
+ Object.assign(this.getParams(), params);
159
163
  store();
160
164
  }
161
165
  push(params: UpdateHistoryParams) {
@@ -166,7 +170,7 @@ class GemHistory extends EventTarget {
166
170
  }
167
171
  pushIgnoreCloseHandle(params: UpdateHistoryParams) {
168
172
  if (store.$hasCloseHandle) {
169
- paramsMap.get(store.$key)?.close?.();
173
+ this.getParams()?.close?.();
170
174
  this.replace(params);
171
175
  } else {
172
176
  this.push(params);
@@ -218,7 +222,7 @@ if (!globalThis._GEMHISTORY) {
218
222
  } else if (nativeHistory.state.$hasCloseHandle) {
219
223
  store(nativeHistory.state);
220
224
  const params = normalizeParams({ title: document.title });
221
- paramsMap.set(store.$key, params);
225
+ getParamsMap().set(store.$key, params);
222
226
  // 有 handle 返回键的页面刷新需要清除返回 handler
223
227
  gemHistory.back();
224
228
  } else {
@@ -228,11 +232,11 @@ if (!globalThis._GEMHISTORY) {
228
232
  $key: getKey(),
229
233
  ...(nativeHistory.state || {}),
230
234
  });
231
- paramsMap.set(store.$key, params);
235
+ getParamsMap().set(store.$key, params);
232
236
  }
233
237
 
234
238
  connect(gemTitleStore, () => {
235
- const params = paramsMap.get(store.$key);
239
+ const params = gemHistory.getParams();
236
240
  if (params) {
237
241
  params.title = gemTitleStore.title;
238
242
  }
@@ -266,9 +270,9 @@ if (!globalThis._GEMHISTORY) {
266
270
  // 刷新后再导航需要从当前 state 中构建 params
267
271
  // 理论上该条历史记录中不会出现事件处理器
268
272
  // 子 app 导航也需要从 state 中构建 params
269
- if (!paramsMap.has(newState.$key)) {
273
+ if (!getParamsMap().has(newState.$key)) {
270
274
  const { pathname, search, hash } = location;
271
- paramsMap.set(newState.$key, {
275
+ getParamsMap().set(newState.$key, {
272
276
  path: getInternalPath(pathname),
273
277
  query: new QueryString(search),
274
278
  hash: decodeURIComponent(hash),
@@ -284,9 +288,9 @@ if (!globalThis._GEMHISTORY) {
284
288
  // 返回键关闭的 modal 能前进键重新打开
285
289
  // 刷新后不能工作:刷新后 historyItem 中只有 url
286
290
  // 能在子 app 中工作
287
- paramsMap.get(newState.$key)?.open?.();
291
+ getParamsMap().get(newState.$key)?.open?.();
288
292
  } else if (prevState.$hasCloseHandle) {
289
- const prevParams = paramsMap.get(prevState.$key);
293
+ const prevParams = getParamsMap().get(prevState.$key);
290
294
  const closeHandle = prevParams?.close;
291
295
  const shouldCloseHandle = prevParams?.shouldClose;
292
296
  const notAllowClose = shouldCloseHandle && !shouldCloseHandle();
@@ -245,8 +245,10 @@ function trustFromTemplateString(tsa: TemplateStringsArray, stringFromTSA: strin
245
245
  * @return Array containing `[html, attrNames]` (array returned for terseness,
246
246
  * to avoid object fields since this code is shared with non-minified SSR
247
247
  * code)
248
+ *
249
+ * Gem Note: export for SSR
248
250
  */
249
- const getTemplateHtml = (strings: TemplateStringsArray, type: ResultType): [TrustedHTML, Array<string>] => {
251
+ export const getTemplateHtml = (strings: TemplateStringsArray, type: ResultType): [TrustedHTML, Array<string>] => {
250
252
  // Insert makers into the template HTML to represent the position of
251
253
  // bindings. The following code scans the template strings to determine the
252
254
  // syntactic position of the bindings. They can be in text position, where