@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.
- package/dist/gem.js +7 -7
- package/dist/gem.mjs +7 -7
- package/elements/base/gesture.d.ts +7 -1
- package/elements/base/gesture.d.ts.map +1 -1
- package/elements/base/gesture.js +23 -15
- package/elements/base/gesture.js.map +1 -1
- package/elements/base/link.d.ts.map +1 -1
- package/elements/base/link.js +6 -3
- package/elements/base/link.js.map +1 -1
- package/elements/base/modal-factory.d.ts +1 -1
- package/elements/base/modal-factory.d.ts.map +1 -1
- package/elements/base/use.d.ts.map +1 -1
- package/elements/base/use.js +11 -0
- package/elements/base/use.js.map +1 -1
- package/helper/hmr.d.ts +3 -2
- package/helper/hmr.d.ts.map +1 -1
- package/helper/hmr.js +95 -23
- package/helper/hmr.js.map +1 -1
- package/helper/i18n.d.ts +2 -1
- package/helper/i18n.d.ts.map +1 -1
- package/helper/i18n.js +9 -1
- package/helper/i18n.js.map +1 -1
- package/helper/theme.d.ts.map +1 -1
- package/helper/theme.js +5 -0
- package/helper/theme.js.map +1 -1
- package/lib/decorators.d.ts +9 -1
- package/lib/decorators.d.ts.map +1 -1
- package/lib/decorators.js +4 -3
- package/lib/decorators.js.map +1 -1
- package/lib/history.d.ts +10 -7
- package/lib/history.d.ts.map +1 -1
- package/lib/history.js +20 -16
- package/lib/history.js.map +1 -1
- package/lib/lit-html.d.ts +15 -0
- package/lib/lit-html.d.ts.map +1 -1
- package/lib/lit-html.js +3 -1
- package/lib/lit-html.js.map +1 -1
- package/lib/reactive.d.ts +4 -1
- package/lib/reactive.d.ts.map +1 -1
- package/lib/reactive.js +41 -18
- package/lib/reactive.js.map +1 -1
- package/lib/server.d.ts +14 -0
- package/lib/server.d.ts.map +1 -0
- package/lib/server.js +43 -0
- package/lib/server.js.map +1 -0
- package/lib/store.d.ts +10 -1
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +36 -9
- package/lib/store.js.map +1 -1
- package/lib/utils.d.ts +2 -2
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +20 -7
- package/lib/utils.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/elements/base/gesture.ts +26 -15
- package/src/elements/base/link.ts +5 -3
- package/src/elements/base/use.ts +11 -0
- package/src/helper/hmr.ts +105 -37
- package/src/helper/i18n.ts +12 -1
- package/src/helper/theme.ts +4 -0
- package/src/lib/decorators.ts +10 -10
- package/src/lib/history.ts +20 -16
- package/src/lib/lit-html.ts +3 -1
- package/src/lib/reactive.ts +40 -18
- package/src/lib/server.ts +45 -0
- package/src/lib/store.ts +40 -9
- package/src/lib/utils.ts +22 -7
- package/src/lib/version.ts +1 -1
- package/src/test/elements/gesture.test.ts +26 -1
- package/src/test/gem-element/basic.test.ts +20 -0
- 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
|
|
58
|
-
getMovementY?: (
|
|
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
|
|
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
|
-
|
|
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
|
-
(
|
|
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 = (
|
|
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
|
-
(
|
|
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
|
|
241
|
-
y: this.#getMovementY(
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 = () => {
|
package/src/elements/base/use.ts
CHANGED
|
@@ -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
|
-
/**
|
|
46
|
+
/** 连接的 store 必须是同一对象,否则元素订阅的是老 store */
|
|
45
47
|
function checkMetadataNeedReload({ mode, penetrable, noBlocking, observedStores }: Metadata, newMetadata: Metadata) {
|
|
46
|
-
const
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]) => ({
|
|
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) =>
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
});
|
package/src/helper/i18n.ts
CHANGED
|
@@ -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 });
|
package/src/helper/theme.ts
CHANGED
|
@@ -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
|
package/src/lib/decorators.ts
CHANGED
|
@@ -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,
|
|
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, (
|
|
304
|
-
|
|
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
|
-
|
|
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`,
|
package/src/lib/history.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
156
|
+
Object.assign(this.getParams(), { path: getInternalPath(location.pathname) });
|
|
153
157
|
}
|
|
154
158
|
getParams() {
|
|
155
|
-
return
|
|
159
|
+
return getParamsMap().get(store.$key)!;
|
|
156
160
|
}
|
|
157
161
|
updateParams(params: UpdateHistoryParams) {
|
|
158
|
-
Object.assign(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
235
|
+
getParamsMap().set(store.$key, params);
|
|
232
236
|
}
|
|
233
237
|
|
|
234
238
|
connect(gemTitleStore, () => {
|
|
235
|
-
const params =
|
|
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 (!
|
|
273
|
+
if (!getParamsMap().has(newState.$key)) {
|
|
270
274
|
const { pathname, search, hash } = location;
|
|
271
|
-
|
|
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
|
-
|
|
291
|
+
getParamsMap().get(newState.$key)?.open?.();
|
|
288
292
|
} else if (prevState.$hasCloseHandle) {
|
|
289
|
-
const prevParams =
|
|
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();
|
package/src/lib/lit-html.ts
CHANGED
|
@@ -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
|