@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/lib/history.ts
CHANGED
|
@@ -52,13 +52,14 @@ const globalParamsMap = new Map<string, HistoryParams>();
|
|
|
52
52
|
const copyParamsMap = () => new Map([...globalParamsMap].map(([k, v]) => [k, { ...v }]));
|
|
53
53
|
const getParamsMap = () => getRequestValue(globalParamsMap, copyParamsMap);
|
|
54
54
|
|
|
55
|
+
// 内部状态字段,不能由外部设置
|
|
56
|
+
const internalStateKeys = ['$key', '$hasCloseHandle', '$hasOpenHandle', '$hasShouldCloseHandle'] as const;
|
|
57
|
+
|
|
55
58
|
function validData(data: any) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if ($hasOpenHandle) throw new GemError('`$hasOpenHandle` is not allowed');
|
|
61
|
-
if ($hasShouldCloseHandle) throw new GemError('`$hasShouldCloseHandle` is not allowed');
|
|
59
|
+
if (store.$key === data?.$key) return;
|
|
60
|
+
for (const key of internalStateKeys) {
|
|
61
|
+
if (data?.[key]) throw new GemError(`\`${key}\` is not allowed`);
|
|
62
|
+
}
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
function getUrlBarPath(internalPath: string) {
|
|
@@ -70,13 +71,17 @@ function getInternalPath(urlBarPath: string) {
|
|
|
70
71
|
return urlBarPath.replace(new RegExp(`^${gemHistory.basePath}/`), '/');
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
// 切换到新的历史记录状态
|
|
75
|
+
function changeState(state: HistoryState) {
|
|
74
76
|
gemHistory.dispatchEvent(new CustomEvent('beforechange'));
|
|
77
|
+
cleanObject(store);
|
|
78
|
+
store(state);
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
function normalizeParams(params: UpdateHistoryParams): HistoryParams {
|
|
78
82
|
const current =
|
|
79
|
-
gemHistory.getParams() ||
|
|
83
|
+
gemHistory.getParams() ||
|
|
84
|
+
({ path: getInternalPath(location.pathname), query: new QueryString(), title: document.title } as HistoryParams);
|
|
80
85
|
// 没提供 path 使用当前 path
|
|
81
86
|
const path = params.path ? absoluteLocation(current.path, params.path) : getInternalPath(location.pathname);
|
|
82
87
|
// 没提供 query 又没有提供 path 时使用当前 search
|
|
@@ -84,8 +89,9 @@ function normalizeParams(params: UpdateHistoryParams): HistoryParams {
|
|
|
84
89
|
const queryChanged = String(newQueryObject) !== String(current.query);
|
|
85
90
|
const query = queryChanged ? newQueryObject : current.query;
|
|
86
91
|
const urlChanged = path !== current.path || queryChanged;
|
|
87
|
-
// 没提供 title 又没有改变 URL 时使用当前
|
|
88
|
-
|
|
92
|
+
// 没提供 title 又没有改变 URL 时使用当前 title
|
|
93
|
+
// 不使用 document.title,它可能已被格式化(例如添加了网站名后缀)
|
|
94
|
+
const title = params.title || (urlChanged ? '' : current.title);
|
|
89
95
|
const statusChanged = params.close || params.data || params.open || params.shouldClose;
|
|
90
96
|
// 没提供 hash 又没有改变 URL 仅仅是状态更新时使用当前 hash
|
|
91
97
|
const hash = decodeURIComponent(params.hash ?? (!urlChanged && statusChanged ? location.hash : ''));
|
|
@@ -105,9 +111,7 @@ function updateHistory(p: UpdateHistoryParams, native: typeof nativeHistory.push
|
|
|
105
111
|
...data,
|
|
106
112
|
};
|
|
107
113
|
getParamsMap().set(state.$key, params);
|
|
108
|
-
|
|
109
|
-
cleanObject(store);
|
|
110
|
-
store(state);
|
|
114
|
+
changeState(state);
|
|
111
115
|
const url = getUrlBarPath(path) + new QueryString(query) + hash;
|
|
112
116
|
const prevHash = decodeURIComponent(location.hash);
|
|
113
117
|
native(state, title, url);
|
|
@@ -116,23 +120,8 @@ function updateHistory(p: UpdateHistoryParams, native: typeof nativeHistory.push
|
|
|
116
120
|
|
|
117
121
|
// 跨框架时,调用者对 basePath 无感知
|
|
118
122
|
function updateHistoryByNative(data: any, title: string, originUrl: string, native: typeof nativeHistory.pushState) {
|
|
119
|
-
validData(data);
|
|
120
|
-
const state = {
|
|
121
|
-
$key: getKey(),
|
|
122
|
-
$title: title,
|
|
123
|
-
...(data || {}),
|
|
124
|
-
};
|
|
125
123
|
const { pathname, search, hash } = new URL(originUrl, location.origin + location.pathname);
|
|
126
|
-
|
|
127
|
-
getParamsMap().set(state.$key, params);
|
|
128
|
-
dispatchBeforeChangeEvent();
|
|
129
|
-
cleanObject(store);
|
|
130
|
-
store(state);
|
|
131
|
-
const url = getUrlBarPath(pathname) + params.query + hash;
|
|
132
|
-
const prevHash = location.hash;
|
|
133
|
-
native(state, title, url);
|
|
134
|
-
// `location.hash` 和 `hash` 都已经进行了 url 编码,可以直接进行相等判断
|
|
135
|
-
if (prevHash !== hash) dispatchEvent(new CustomEvent('hashchange'));
|
|
124
|
+
updateHistory({ path: pathname, query: search, hash, title, data }, native);
|
|
136
125
|
}
|
|
137
126
|
|
|
138
127
|
const gemBasePathStore = createStore({
|
|
@@ -313,9 +302,7 @@ if (!globalThis._GEMHISTORY) {
|
|
|
313
302
|
}
|
|
314
303
|
}
|
|
315
304
|
|
|
316
|
-
|
|
317
|
-
cleanObject(store);
|
|
318
|
-
store(newState);
|
|
305
|
+
changeState(newState);
|
|
319
306
|
});
|
|
320
307
|
}
|
|
321
308
|
|
package/src/lib/reactive.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// biome-ignore-all lint/plugin/no_this_effect: 兼容生命周期
|
|
2
2
|
|
|
3
3
|
import type * as Lit from './lit-html';
|
|
4
|
-
import { isServer } from './server';
|
|
4
|
+
import { isServer, ssrHooks } from './server';
|
|
5
5
|
import type { Store } from './store';
|
|
6
6
|
import { connect } from './store';
|
|
7
7
|
import {
|
|
@@ -27,21 +27,9 @@ export function setEngine(h: typeof Lit.html, r: typeof Lit.render) {
|
|
|
27
27
|
render = r;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
return;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// 在元素首次渲染前恢复 SSR 的状态
|
|
35
|
-
let restoreState: ((ele: GemElement) => void) | undefined;
|
|
36
|
-
|
|
37
|
-
export function setHydrationEngine(engine: typeof hydrationEngine, restore?: typeof restoreState) {
|
|
38
|
-
hydrationEngine = engine;
|
|
39
|
-
restoreState = restore;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** 渲染元素内容,SSR 时由水合引擎接管 */
|
|
30
|
+
/** 渲染元素内容,SSR 时由 `ssrHooks.render` 接管 */
|
|
43
31
|
export const _renderRoot = (value: unknown, container: HTMLElement | DocumentFragment) =>
|
|
44
|
-
|
|
32
|
+
ssrHooks.render?.(value, container) || render(value, container);
|
|
45
33
|
|
|
46
34
|
const { assign, defineProperty } = Object;
|
|
47
35
|
|
|
@@ -59,14 +47,12 @@ export const UpdateToken = Symbol.for('gem@update');
|
|
|
59
47
|
const BoundaryCSSState = 'gem-style-boundary';
|
|
60
48
|
const ElementCSSState = 'gem-element';
|
|
61
49
|
|
|
62
|
-
|
|
50
|
+
/** 渲染出错时在元素上派发,`@fallback` 处理时取消,SSR 时 `@server` 方法出错也会派发 */
|
|
63
51
|
export const _RenderErrorEvent = 'gem@render-error';
|
|
64
52
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const rootStyleSheetInfo = new WeakMap<Document | ShadowRoot, Map<CSSStyleSheet, number>>();
|
|
69
|
-
const rootUpdateFnMap = new WeakMap<Map<CSSStyleSheet, number>, () => void>();
|
|
53
|
+
// 渲染根中 light DOM 元素使用的样式表和使用次数,以及更新渲染根样式表的函数
|
|
54
|
+
type RootSheets = { counts: Map<CSSStyleSheet, number>; update: () => void };
|
|
55
|
+
const rootSheetsMap = new WeakMap<Document | ShadowRoot, RootSheets>();
|
|
70
56
|
|
|
71
57
|
class GemCSSStyleSheet extends CSSStyleSheet {
|
|
72
58
|
#ele?: HTMLStyleElement;
|
|
@@ -96,45 +82,37 @@ class GemCSSSheet {
|
|
|
96
82
|
this.#content = v;
|
|
97
83
|
}
|
|
98
84
|
|
|
99
|
-
//
|
|
100
|
-
#
|
|
101
|
-
|
|
85
|
+
// 对同一类 dom 只使用同一个样式表,key 是元素类或者 `this`,不需要 GC
|
|
86
|
+
#sheets = new Map<any, { sheet: GemCSSStyleSheet; scope: string }>();
|
|
87
|
+
|
|
88
|
+
#apply({ sheet, scope }: { sheet: GemCSSStyleSheet; scope: string }) {
|
|
89
|
+
sheet.replaceSync(scope ? `${scope}{ ${this.#content} }` : this.#content);
|
|
90
|
+
}
|
|
91
|
+
|
|
102
92
|
getStyle(host?: HTMLElement, isEleTheme?: boolean) {
|
|
103
93
|
const isLight = host && !(host as GemElement).internals?.shadowRoot;
|
|
104
|
-
|
|
105
|
-
// 对同一类 dom 只使用同一个样式表
|
|
106
94
|
const key = isLight ? host.constructor : this;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
scope = isEleTheme ? '@scope' : `@scope (${host.tagName}) to (:state(${BoundaryCSSState}) > *)`;
|
|
122
|
-
// 不能使用 @layer,两个 @layer 不能覆盖,只有顺序起作用
|
|
123
|
-
// 所以外部不能通过元素名称选择器来覆盖样式,除非样式在之前插入(会自动反转应用到尾部, see `appleCSSStyleSheet`)
|
|
124
|
-
style = `${scope}{ ${style} }`;
|
|
125
|
-
}
|
|
126
|
-
sheet.replaceSync(style);
|
|
127
|
-
this.#used.set(sheet, scope);
|
|
95
|
+
let item = this.#sheets.get(key);
|
|
96
|
+
if (!item) {
|
|
97
|
+
// light dom 嵌套时需要选择子元素
|
|
98
|
+
// `> *` 实际上是多范围?是否存在性能问题
|
|
99
|
+
// 不能使用 @layer,两个 @layer 不能覆盖,只有顺序起作用
|
|
100
|
+
// 所以外部不能通过元素名称选择器来覆盖样式,除非样式在之前插入(会自动反转应用到尾部, see `adoptRootStyleSheets`)
|
|
101
|
+
const scope = !isLight
|
|
102
|
+
? ''
|
|
103
|
+
: isEleTheme
|
|
104
|
+
? '@scope'
|
|
105
|
+
: `@scope (${host.tagName}) to (:state(${BoundaryCSSState}) > *)`;
|
|
106
|
+
item = { sheet: new GemCSSStyleSheet({ media: this.#media }), scope };
|
|
107
|
+
this.#sheets.set(key, item);
|
|
108
|
+
this.#apply(item);
|
|
128
109
|
}
|
|
129
|
-
|
|
130
|
-
return sheet;
|
|
110
|
+
return item.sheet;
|
|
131
111
|
}
|
|
132
112
|
|
|
133
113
|
// 一般用于主题更新
|
|
134
114
|
updateStyle() {
|
|
135
|
-
this.#
|
|
136
|
-
sheet.replaceSync(scope ? `${scope}{${this.#content}}` : this.#content);
|
|
137
|
-
});
|
|
115
|
+
this.#sheets.forEach((item) => this.#apply(item));
|
|
138
116
|
}
|
|
139
117
|
}
|
|
140
118
|
|
|
@@ -182,40 +160,38 @@ export function css<T extends Record<string, string>>(
|
|
|
182
160
|
return sheet as Sheet<T>;
|
|
183
161
|
}
|
|
184
162
|
|
|
185
|
-
const
|
|
163
|
+
const updateRootSheets = ({ counts, update }: RootSheets, sheets: CSSStyleSheet[], value: number) => {
|
|
186
164
|
let needUpdate = false;
|
|
187
165
|
sheets.forEach((e) => {
|
|
188
|
-
const count =
|
|
166
|
+
const count = counts.get(e) || 0;
|
|
189
167
|
needUpdate ||= (value === 1 && count === 0) || (value === -1 && count === 1);
|
|
190
168
|
// count 为 0 时不立即删除,以便更新时识别是否外部样式
|
|
191
169
|
// 但是只要挂载 document 的样式表就不会被 GC 了, 是否要在闲时 GC?
|
|
192
|
-
|
|
170
|
+
counts.set(e, count + value);
|
|
193
171
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
addMicrotask(rootUpdateFnMap.get(map)!);
|
|
198
|
-
}
|
|
172
|
+
// 避免重复更新,例如 list 元素增删,全 light dom 时 document 更新
|
|
173
|
+
// 在 SSR 中应该区别 effect 强制执行
|
|
174
|
+
if (needUpdate) addMicrotask(update);
|
|
199
175
|
};
|
|
200
176
|
|
|
201
|
-
|
|
177
|
+
/** light DOM 元素的样式表添加到渲染根中,返回移除函数 */
|
|
178
|
+
const adoptRootStyleSheets = (ele: HTMLElement, sheets: CSSStyleSheet[]) => {
|
|
202
179
|
const root = ele.getRootNode() as ShadowRoot | Document;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
180
|
+
let rootSheets = rootSheetsMap.get(root);
|
|
181
|
+
if (!rootSheets) {
|
|
182
|
+
const counts = new Map<CSSStyleSheet, number>();
|
|
183
|
+
const update = () => {
|
|
207
184
|
// 先找到外部样式表
|
|
208
|
-
const newSheets = (root.adoptedStyleSheets || []).filter((e) => !
|
|
209
|
-
|
|
185
|
+
const newSheets = (root.adoptedStyleSheets || []).filter((e) => !counts.has(e));
|
|
186
|
+
counts.forEach((count, sheet) => count && newSheets.push(sheet));
|
|
210
187
|
// 外层元素的样式要放到最后,以提升优先级,但是只考虑第一次出现样式表的位置
|
|
211
188
|
root.adoptedStyleSheets = newSheets.reverse();
|
|
212
|
-
}
|
|
189
|
+
};
|
|
190
|
+
rootSheets = { counts, update };
|
|
191
|
+
rootSheetsMap.set(root, rootSheets);
|
|
213
192
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
updateStyleSheets(map, sheets, 1);
|
|
218
|
-
return () => updateStyleSheets(map, sheets, -1);
|
|
193
|
+
updateRootSheets(rootSheets, sheets, 1);
|
|
194
|
+
return () => updateRootSheets(rootSheets, sheets, -1);
|
|
219
195
|
};
|
|
220
196
|
|
|
221
197
|
/**必须使用在字段中,否则会读取到错误的实例 */
|
|
@@ -232,23 +208,25 @@ type EffectItem<T> = {
|
|
|
232
208
|
preCallback?: any;
|
|
233
209
|
};
|
|
234
210
|
|
|
235
|
-
function
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
211
|
+
function runEffect<T>(item: EffectItem<T>, values: T) {
|
|
212
|
+
execCallback(item.preCallback);
|
|
213
|
+
item.preCallback = item.callback(values, item.values);
|
|
214
|
+
item.values = values;
|
|
215
|
+
// 更新时也要设置,避免异步的 `#initEffect` 重复执行
|
|
216
|
+
item.initialized = true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function execEffectList(list: EffectItem<any>[]) {
|
|
220
|
+
list.forEach((item) => {
|
|
221
|
+
const values = item.getDep?.();
|
|
222
|
+
if (!item.getDep || !item.values || isArrayChange(item.values, values)) runEffect(item, values);
|
|
246
223
|
});
|
|
247
224
|
}
|
|
248
225
|
|
|
249
226
|
function clearEffect(list: EffectItem<any>[]) {
|
|
250
227
|
return list.filter((e) => {
|
|
251
228
|
execCallback(e.preCallback);
|
|
229
|
+
e.preCallback = undefined;
|
|
252
230
|
e.initialized = false;
|
|
253
231
|
e.values = undefined;
|
|
254
232
|
return e.fixed;
|
|
@@ -277,6 +255,8 @@ export type Metadata = Partial<ShadowRootInit> & {
|
|
|
277
255
|
>;
|
|
278
256
|
// 实例化时使用到,DevTools 需要读取
|
|
279
257
|
observedStores?: Store<any>[];
|
|
258
|
+
/** attribute 对应的字段名,用于 `setAttribute` 等方法设置字段 */
|
|
259
|
+
attrProps?: Record<string, string>;
|
|
280
260
|
adoptedStyleSheets?: Sheet<unknown>[];
|
|
281
261
|
// 以下静态字段仅供外部读取,没有实际作用
|
|
282
262
|
observedProperties?: string[];
|
|
@@ -285,8 +265,6 @@ export type Metadata = Partial<ShadowRootInit> & {
|
|
|
285
265
|
definedCSSStates?: string[];
|
|
286
266
|
definedParts?: string[];
|
|
287
267
|
definedSlots?: string[];
|
|
288
|
-
/**@deprecated GemDevtools 在使用*/
|
|
289
|
-
definedRefs?: string[];
|
|
290
268
|
};
|
|
291
269
|
|
|
292
270
|
// global render task pool
|
|
@@ -340,9 +318,8 @@ export abstract class GemElement extends HTMLElement {
|
|
|
340
318
|
#rendering?: boolean;
|
|
341
319
|
#clearStyle?: (() => void) | undefined;
|
|
342
320
|
|
|
343
|
-
[
|
|
344
|
-
|
|
345
|
-
if (this.#isMounted) addMicrotask(this.#update);
|
|
321
|
+
[UpdateToken]() {
|
|
322
|
+
this.#scheduleUpdate();
|
|
346
323
|
}
|
|
347
324
|
|
|
348
325
|
static {
|
|
@@ -357,9 +334,8 @@ export abstract class GemElement extends HTMLElement {
|
|
|
357
334
|
}
|
|
358
335
|
assignState(state, payload);
|
|
359
336
|
// 避免无限刷新
|
|
360
|
-
// 挂载前 set state 不应该触发更新
|
|
361
337
|
// SSR 中渲染 <gem-route>,其他情况的 SSR 应该也可以执行
|
|
362
|
-
if (!ele.#rendering
|
|
338
|
+
if (!ele.#rendering) ele.#scheduleUpdate();
|
|
363
339
|
});
|
|
364
340
|
ele.#internals.stateList.push(state);
|
|
365
341
|
return state;
|
|
@@ -378,6 +354,10 @@ export abstract class GemElement extends HTMLElement {
|
|
|
378
354
|
// 这里认为 rest 不会有自定义字段和 ShadowRootInit 参数冲突
|
|
379
355
|
this.#renderRoot = !mode ? this : this.shadowRoot || this.attachShadow({ mode, delegatesFocus, ...rest });
|
|
380
356
|
this.#internals = this.attachInternals() as GemElement['internals'];
|
|
357
|
+
// 定义前添加的 ShadowRoot 对 `ElementInternals` 不可见,例如客户端填充了服务端内容的延时视图元素
|
|
358
|
+
if (this.#renderRoot !== this && !this.#internals.shadowRoot) {
|
|
359
|
+
defineProperty(this.#internals, 'shadowRoot', { value: this.#renderRoot });
|
|
360
|
+
}
|
|
381
361
|
this.#internals.stateList = [];
|
|
382
362
|
this.#internals.sheets = [];
|
|
383
363
|
|
|
@@ -423,10 +403,6 @@ export abstract class GemElement extends HTMLElement {
|
|
|
423
403
|
execEffectList(this.#effectList);
|
|
424
404
|
};
|
|
425
405
|
|
|
426
|
-
#execMemo = () => {
|
|
427
|
-
execEffectList(this.#memoList);
|
|
428
|
-
};
|
|
429
|
-
|
|
430
406
|
/**
|
|
431
407
|
* - 条件渲染
|
|
432
408
|
* - 没有 `render` 时显示子内容
|
|
@@ -437,7 +413,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
437
413
|
#render = (item?: RenderItem) => {
|
|
438
414
|
try {
|
|
439
415
|
this.#rendering = true;
|
|
440
|
-
this.#
|
|
416
|
+
execEffectList(this.#memoList);
|
|
441
417
|
const isLight = this.#renderRoot === this;
|
|
442
418
|
const temp = item ? item.render() : isLight ? undefined : html`<slot></slot>`;
|
|
443
419
|
if (temp === undefined) return;
|
|
@@ -469,15 +445,22 @@ export abstract class GemElement extends HTMLElement {
|
|
|
469
445
|
}
|
|
470
446
|
};
|
|
471
447
|
|
|
472
|
-
|
|
473
|
-
|
|
448
|
+
// `@async()` 的元素在空闲时执行,SSR 需要同步执行
|
|
449
|
+
#schedule = (task: () => void) => {
|
|
474
450
|
if (this.#metadata.noBlocking && !isServer) {
|
|
475
|
-
noBlockingTaskList.add(
|
|
451
|
+
noBlockingTaskList.add(task);
|
|
476
452
|
} else {
|
|
477
|
-
|
|
453
|
+
task();
|
|
478
454
|
}
|
|
479
455
|
};
|
|
480
456
|
|
|
457
|
+
#update = () => this.#schedule(this.#updateCallback);
|
|
458
|
+
|
|
459
|
+
// 挂载前(例如 `connectedCallback` 中的 property 赋值)不需要更新,挂载时会渲染
|
|
460
|
+
#scheduleUpdate = () => {
|
|
461
|
+
if (this.#isMounted) addMicrotask(this.#update);
|
|
462
|
+
};
|
|
463
|
+
|
|
481
464
|
#prepareStyle = () => {
|
|
482
465
|
const { adoptedStyleSheets = [] } = this.#metadata;
|
|
483
466
|
const { shadowRoot, sheets } = this.#internals;
|
|
@@ -487,7 +470,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
487
470
|
shadowRoot.adoptedStyleSheets = clsSheets.concat(sheets);
|
|
488
471
|
} else {
|
|
489
472
|
this.prepend(...sheets.map((e) => e.element));
|
|
490
|
-
return
|
|
473
|
+
return adoptRootStyleSheets(this, clsSheets);
|
|
491
474
|
}
|
|
492
475
|
};
|
|
493
476
|
|
|
@@ -495,14 +478,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
495
478
|
* 元素挂载后执行还未初始化的副作用
|
|
496
479
|
* */
|
|
497
480
|
#initEffect = () => {
|
|
498
|
-
this.#effectList
|
|
499
|
-
const { callback, getDep, initialized } = effectItem;
|
|
500
|
-
if (!initialized) {
|
|
501
|
-
effectItem.values = getDep?.();
|
|
502
|
-
effectItem.preCallback = callback(effectItem.values);
|
|
503
|
-
effectItem.initialized = true;
|
|
504
|
-
}
|
|
505
|
-
});
|
|
481
|
+
this.#effectList.forEach((item) => !item.initialized && runEffect(item, item.getDep?.()));
|
|
506
482
|
};
|
|
507
483
|
|
|
508
484
|
/** @lifecycle 将来会彻底移除,现在做兼容实现*/ willMount?(): void | Promise<void>;
|
|
@@ -534,6 +510,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
534
510
|
this.#clearStyle = this.#prepareStyle();
|
|
535
511
|
return;
|
|
536
512
|
}
|
|
513
|
+
if (ssrHooks.connect?.(this, this.#connectedCallback)) return;
|
|
537
514
|
this.#notInCons = true;
|
|
538
515
|
this.#compat();
|
|
539
516
|
|
|
@@ -546,7 +523,6 @@ export abstract class GemElement extends HTMLElement {
|
|
|
546
523
|
if (!mode && this.#renderList.length && !penetrable) this.#internals.states.add(BoundaryCSSState);
|
|
547
524
|
|
|
548
525
|
this.#disconnectStore = observedStores?.map((store) => connect(store, this.#update));
|
|
549
|
-
restoreState?.(this);
|
|
550
526
|
this.#render(this.#renderItem);
|
|
551
527
|
this.#isMounted = true;
|
|
552
528
|
this.#clearStyle = this.#prepareStyle();
|
|
@@ -562,11 +538,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
562
538
|
* use `@mounted`
|
|
563
539
|
*/
|
|
564
540
|
connectedCallback(): Unique {
|
|
565
|
-
|
|
566
|
-
noBlockingTaskList.add(this.#connectedCallback);
|
|
567
|
-
} else {
|
|
568
|
-
this.#connectedCallback();
|
|
569
|
-
}
|
|
541
|
+
this.#schedule(this.#connectedCallback);
|
|
570
542
|
return undefined as unknown as Unique;
|
|
571
543
|
}
|
|
572
544
|
|
|
@@ -606,17 +578,9 @@ export abstract class GemElement extends HTMLElement {
|
|
|
606
578
|
* ```
|
|
607
579
|
* */
|
|
608
580
|
effect = <K = any[] | undefined>(callback: EffectCallback<K>, getDep?: K extends any[] ? () => K : undefined) => {
|
|
609
|
-
const effectItem: EffectItem<K> = {
|
|
610
|
-
callback,
|
|
611
|
-
getDep,
|
|
612
|
-
initialized: this.#isMounted,
|
|
613
|
-
fixed: !this.#notInCons,
|
|
614
|
-
};
|
|
581
|
+
const effectItem: EffectItem<K> = { callback, getDep, fixed: !this.#notInCons };
|
|
615
582
|
// 已挂载时立即执行副作用,未挂载时等挂载后执行
|
|
616
|
-
if (this.#isMounted)
|
|
617
|
-
effectItem.values = getDep?.() as K;
|
|
618
|
-
effectItem.preCallback = callback(effectItem.values);
|
|
619
|
-
}
|
|
583
|
+
if (this.#isMounted) runEffect(effectItem, getDep?.() as K);
|
|
620
584
|
this.#effectList.push(effectItem);
|
|
621
585
|
};
|
|
622
586
|
|
|
@@ -649,7 +613,7 @@ export abstract class GemElement extends HTMLElement {
|
|
|
649
613
|
* @helper
|
|
650
614
|
* async
|
|
651
615
|
*/
|
|
652
|
-
update = () =>
|
|
616
|
+
update = () => this.#scheduleUpdate();
|
|
653
617
|
}
|
|
654
618
|
|
|
655
619
|
// 用来禁止 connectCallback 方法被覆盖而破坏 GemElement 的功能
|
package/src/lib/server.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// `@mantou/gem-ssr` 在加载 Gem 前提供 `__GEM_SSR__`,返回当前请求的作用域,
|
|
4
4
|
// 客户端构建时 `unplugin-gem` 把 `globalThis.__GEM_SSR__` 替换成 `undefined`,SSR 代码会被压缩移除
|
|
5
5
|
|
|
6
|
+
import type { GemElement } from './reactive';
|
|
7
|
+
|
|
6
8
|
const { get, set, has, deleteProperty, defineProperty, getOwnPropertyDescriptor, ownKeys } = Reflect;
|
|
7
9
|
|
|
8
10
|
const getRequestScope: (() => WeakMap<object, any> | undefined) | undefined = (globalThis as any).__GEM_SSR__;
|
|
@@ -10,6 +12,18 @@ const getRequestScope: (() => WeakMap<object, any> | undefined) | undefined = (g
|
|
|
10
12
|
/** 服务端渲染时不执行副作用,同步更新 */
|
|
11
13
|
export const isServer = !!getRequestScope;
|
|
12
14
|
|
|
15
|
+
/** SSR 接入 Gem 的钩子,`@mantou/gem-ssr` 在服务端和客户端分别设置 */
|
|
16
|
+
export const ssrHooks: {
|
|
17
|
+
/** 渲染元素内容,返回 `true` 时跳过渲染:服务端收集模版,客户端水合 */
|
|
18
|
+
render?: (value: unknown, container: HTMLElement | DocumentFragment) => boolean;
|
|
19
|
+
/** 元素挂载前调用,返回 `true` 时推迟挂载,之后调用 `connect` 挂载:客户端恢复状态,等待父模版水合 */
|
|
20
|
+
connect?: (ele: GemElement, connect: () => void) => boolean;
|
|
21
|
+
/** 创建有 key 的 store,返回初始值:客户端恢复服务端的值 */
|
|
22
|
+
createStore?: (key: string) => object | undefined;
|
|
23
|
+
/** 更新有 key 的 store:服务端记录,渲染后输出到页面中 */
|
|
24
|
+
updateStore?: (key: string, store: object) => void;
|
|
25
|
+
} = {};
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
28
|
* SSR 时返回当前请求中 `key` 对应的值,第一次访问时使用 `init` 创建,
|
|
15
29
|
* 不在请求中(包括客户端)时返回 `key` 本身
|
package/src/lib/store.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getRequestValue, isolate, isServer } from './server';
|
|
1
|
+
import { getRequestValue, isolate, isServer, ssrHooks } from './server';
|
|
2
2
|
import { addMicrotask, assignState, createUpdater, type Updater } from './utils';
|
|
3
3
|
|
|
4
4
|
export type Store<T = any> = Updater<T>;
|
|
@@ -8,20 +8,6 @@ export const _StoreListenerMap = new WeakMap<Store<any>, Set<() => void>>();
|
|
|
8
8
|
|
|
9
9
|
const createListeners = () => new Set<() => void>();
|
|
10
10
|
|
|
11
|
-
// SSR 时当前请求中更新过的有 key 的 store,渲染后输出到页面中
|
|
12
|
-
const updatedStores = new Map<string, Store<any>>();
|
|
13
|
-
|
|
14
|
-
/** SSR 时当前请求中更新过的有 key 的 store,供 `@mantou/gem-ssr` 使用 */
|
|
15
|
-
export const _getUpdatedStores = () => getRequestValue(updatedStores, () => new Map());
|
|
16
|
-
|
|
17
|
-
// 客户端读取服务端渲染时 store 的值
|
|
18
|
-
let restoreStore: ((key: string) => object | undefined) | undefined;
|
|
19
|
-
|
|
20
|
-
/** 设置客户端恢复 store 的方法,供 `@mantou/gem-ssr` 使用 */
|
|
21
|
-
export function _setStoreRestorer(restorer: typeof restoreStore) {
|
|
22
|
-
restoreStore = restorer;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
11
|
/**
|
|
26
12
|
* 创建 store,`key` 用来在 SSR 时同步服务端的值,`swc-plugin-gem` 的 `ssr` 选项会为模块顶层的 store 自动添加
|
|
27
13
|
*/
|
|
@@ -35,12 +21,10 @@ export function createStore<T = any>(originStore: T, key?: string) {
|
|
|
35
21
|
if (!isServer) return;
|
|
36
22
|
// SSR 时请求中添加的监听器只属于当前请求,其他请求更新 store 时不会触发
|
|
37
23
|
getRequestValue(listeners, createListeners).forEach(addMicrotask);
|
|
38
|
-
|
|
39
|
-
// 不在请求中时更新的是全局的值,客户端也一样
|
|
40
|
-
if (key && updated !== updatedStores) updated.set(key, store);
|
|
24
|
+
if (key) ssrHooks.updateStore?.(key, store);
|
|
41
25
|
}),
|
|
42
26
|
);
|
|
43
|
-
if (key
|
|
27
|
+
if (key) assignState(store, ssrHooks.createStore?.(key));
|
|
44
28
|
_StoreListenerMap.set(store, listeners);
|
|
45
29
|
return store;
|
|
46
30
|
}
|
package/src/lib/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.
|
|
1
|
+
export const version = '2.3.1';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EndEventDetail, GemGestureElement, PanEventDetail, SwipeEventDetail } from '../../elements/gesture';
|
|
2
2
|
import { html } from '../../lib/element';
|
|
3
|
-
import { aTimeout, expect, fixture } from '../utils';
|
|
3
|
+
import { aTimeout, createFakeTimers, expect, fixture } from '../utils';
|
|
4
4
|
|
|
5
5
|
import '../../elements/gesture';
|
|
6
6
|
|
|
@@ -20,6 +20,12 @@ function pointer(type: string, [timeStamp, clientX, clientY = 0]: Sample, pointe
|
|
|
20
20
|
return event;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
const clock = createFakeTimers();
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
clock.uninstall();
|
|
27
|
+
});
|
|
28
|
+
|
|
23
29
|
async function setupGesture(touchAction = '') {
|
|
24
30
|
const el: GemGestureElement = await fixture(html`<gem-gesture touch-action=${touchAction}></gem-gesture>`);
|
|
25
31
|
const captured = new Set<number>();
|
|
@@ -43,9 +49,9 @@ describe('GemGestureElement end', () => {
|
|
|
43
49
|
const { el, dispatch } = await setupGesture();
|
|
44
50
|
const ends: EndEventDetail[] = [];
|
|
45
51
|
el.addEventListener('end', (evt: CustomEvent<EndEventDetail>) => ends.push(evt.detail));
|
|
46
|
-
|
|
52
|
+
clock.install();
|
|
47
53
|
dispatch('pointerdown', [0, 0]);
|
|
48
|
-
|
|
54
|
+
clock.tick(251);
|
|
49
55
|
const event = pointer(type, [300, 10, 20]);
|
|
50
56
|
el.dispatchEvent(event);
|
|
51
57
|
await Promise.resolve();
|
|
@@ -68,7 +74,9 @@ describe('GemGestureElement end', () => {
|
|
|
68
74
|
el.addEventListener('end', (evt: CustomEvent<EndEventDetail>) => resolve(evt.detail), { once: true });
|
|
69
75
|
});
|
|
70
76
|
el.addEventListener('press', () => dispatch('pointerup', [300, 0]), { once: true });
|
|
77
|
+
clock.install();
|
|
71
78
|
dispatch('pointerdown', [0, 0]);
|
|
79
|
+
clock.tick(251);
|
|
72
80
|
expect((await ended).pressed).to.equal(true);
|
|
73
81
|
});
|
|
74
82
|
});
|
|
@@ -330,9 +338,9 @@ describe('GemGestureElement swipe', () => {
|
|
|
330
338
|
|
|
331
339
|
it('does not swipe after a long press', async () => {
|
|
332
340
|
const { el, swipes, dispatch } = await setupGesture();
|
|
333
|
-
|
|
341
|
+
clock.install();
|
|
334
342
|
dispatch('pointerdown', [0, 0]);
|
|
335
|
-
|
|
343
|
+
clock.tick(251);
|
|
336
344
|
dispatch('pointermove', [300, 0]);
|
|
337
345
|
dispatch('pointermove', [340, 60]);
|
|
338
346
|
dispatch('pointerup', [350, 60]);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { GemActiveLinkElement } from '../../elements/link';
|
|
1
|
+
import type { GemActiveLinkElement, GemLinkElement } from '../../elements/link';
|
|
2
2
|
import { html } from '../../lib/element';
|
|
3
|
-
import {
|
|
3
|
+
import { history } from '../../lib/history';
|
|
4
|
+
import { expect, fixture, nextFrame } from '../utils';
|
|
4
5
|
|
|
5
6
|
import '../../elements/link';
|
|
6
7
|
|
|
@@ -11,4 +12,13 @@ describe('GemLinkElement', () => {
|
|
|
11
12
|
);
|
|
12
13
|
expect(ele.active).to.equal(true);
|
|
13
14
|
});
|
|
15
|
+
it('basePath', async () => {
|
|
16
|
+
const ele: GemLinkElement = await fixture(html`<gem-link href="/a">a</gem-link>`);
|
|
17
|
+
const getHref = () => new URL(ele.shadowRoot!.querySelector('a')!.href).pathname;
|
|
18
|
+
expect(getHref()).to.equal('/a');
|
|
19
|
+
history.basePath = '/base';
|
|
20
|
+
await nextFrame();
|
|
21
|
+
expect(getHref()).to.equal('/base/a');
|
|
22
|
+
history.basePath = '';
|
|
23
|
+
});
|
|
14
24
|
});
|