@mantou/gem 1.7.8 → 1.7.10
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.es.js +246 -113
- package/dist/gem.umd.js +24 -24
- package/elements/base/dialog.d.ts.map +1 -1
- package/elements/base/dialog.js +1 -1
- package/elements/base/dialog.js.map +1 -1
- package/elements/base/gesture.d.ts.map +1 -1
- package/elements/base/gesture.js +7 -1
- package/elements/base/gesture.js.map +1 -1
- package/elements/base/link.d.ts.map +1 -1
- package/elements/base/link.js +8 -4
- package/elements/base/link.js.map +1 -1
- package/elements/base/modal-factory.d.ts +28 -6
- package/elements/base/modal-factory.d.ts.map +1 -1
- package/elements/base/reflect.d.ts +8 -0
- package/elements/base/reflect.d.ts.map +1 -1
- package/elements/base/reflect.js +36 -9
- package/elements/base/reflect.js.map +1 -1
- package/elements/base/route.d.ts +38 -10
- package/elements/base/route.d.ts.map +1 -1
- package/elements/base/route.js +119 -73
- package/elements/base/route.js.map +1 -1
- package/elements/base/title.d.ts.map +1 -1
- package/elements/base/title.js +8 -2
- package/elements/base/title.js.map +1 -1
- package/helper/i18n.d.ts +8 -1
- package/helper/i18n.d.ts.map +1 -1
- package/helper/i18n.js +7 -3
- package/helper/i18n.js.map +1 -1
- package/helper/logger.d.ts +9 -0
- package/helper/logger.d.ts.map +1 -0
- package/helper/logger.js +16 -0
- package/helper/logger.js.map +1 -0
- package/helper/ssr-shim.d.ts.map +1 -1
- package/helper/ssr-shim.js +12 -4
- package/helper/ssr-shim.js.map +1 -1
- package/lib/element.d.ts +5 -5
- package/lib/element.d.ts.map +1 -1
- package/lib/element.js +40 -14
- package/lib/element.js.map +1 -1
- package/lib/history.d.ts +3 -2
- package/lib/history.d.ts.map +1 -1
- package/lib/history.js +22 -9
- package/lib/history.js.map +1 -1
- package/lib/utils.d.ts +2 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +10 -8
- package/lib/utils.js.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 +2 -2
- package/src/elements/base/dialog.ts +4 -1
- package/src/elements/base/gesture.ts +6 -1
- package/src/elements/base/link.ts +8 -4
- package/src/elements/base/reflect.ts +43 -9
- package/src/elements/base/route.ts +130 -73
- package/src/elements/base/title.ts +9 -2
- package/src/helper/i18n.ts +12 -4
- package/src/helper/logger.ts +21 -0
- package/src/helper/ssr-shim.ts +14 -4
- package/src/lib/element.ts +46 -17
- package/src/lib/history.ts +30 -12
- package/src/lib/utils.ts +16 -9
- package/src/lib/version.ts +1 -1
- package/src/test/elements/link.test.ts +15 -0
- package/src/test/elements/reflect.test.ts +27 -0
- package/src/test/elements/title.test.ts +25 -0
- package/src/test/utils.test.ts +14 -0
|
@@ -1,28 +1,25 @@
|
|
|
1
1
|
import { GemElement, html, TemplateResult } from '../../lib/element';
|
|
2
|
-
import { property,
|
|
3
|
-
import { createStore, updateStore, Store } from '../../lib/store';
|
|
2
|
+
import { property, emitter, Emitter, boolattribute } from '../../lib/decorators';
|
|
3
|
+
import { createStore, updateStore, Store, connect } from '../../lib/store';
|
|
4
4
|
import { titleStore, history, UpdateHistoryParams } from '../../lib/history';
|
|
5
|
-
import { QueryString } from '../../lib/utils';
|
|
5
|
+
import { addListener, QueryString } from '../../lib/utils';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
[index: string]: number;
|
|
9
|
-
}
|
|
7
|
+
const UNNAMED_PARAM_PREFIX = '_';
|
|
10
8
|
|
|
9
|
+
// '/test/:a' /^\/test\/(?<a>[^/]+)$/i
|
|
10
|
+
// '/test/:a/b' /^\/test\/?<a>([^/]+)\/b$/i
|
|
11
|
+
// '/test/*' /^\/test\/(?<_0>.*)$/i
|
|
12
|
+
// '/test/:a/*' /^\/test\/(?<a>[^/]+)\/(?<_0>.*)$/i
|
|
13
|
+
// '/test/*/:b /^\/test\/(?<_0>.*)\/(?<a>[^/]+)$/i
|
|
11
14
|
class ParamsRegExp extends RegExp {
|
|
12
|
-
namePosition: NamePosition;
|
|
13
15
|
constructor(pattern: string) {
|
|
14
|
-
const namePosition: NamePosition = {};
|
|
15
16
|
let i = 0;
|
|
16
17
|
super(
|
|
17
|
-
`^${pattern
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
})
|
|
22
|
-
.replace('*', '.*')}$`,
|
|
23
|
-
'i',
|
|
18
|
+
`^${pattern.replace(/((:[^/$]+)|(\*))/g, (_m, name: string) => {
|
|
19
|
+
if (name === '*') return `(?<${UNNAMED_PARAM_PREFIX}${i++}>.*)`;
|
|
20
|
+
return `(?<${name.slice(1)}>[^/]+)`;
|
|
21
|
+
})}$`,
|
|
24
22
|
);
|
|
25
|
-
this.namePosition = namePosition;
|
|
26
23
|
}
|
|
27
24
|
}
|
|
28
25
|
|
|
@@ -35,12 +32,19 @@ declare global {
|
|
|
35
32
|
}
|
|
36
33
|
}
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
/**
|
|
36
|
+
* - 大小写敏感
|
|
37
|
+
* - 匹配成功时返回 params
|
|
38
|
+
* - 以 `/` 结尾时能不带 `/` 结尾的路径
|
|
39
|
+
* - `*` 匹配到的 params 名称为数字
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```js
|
|
43
|
+
* matchPath(`/a/b/:c/:d`,`/a/b/1/2`);
|
|
44
|
+
* matchPath(`/a/b/:c/:d`,`/a/b/1/2/`);
|
|
45
|
+
* matchPath(`/a/b/*`,`/a/b/1/2/`);
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
44
48
|
export function matchPath(pattern: string, path: string) {
|
|
45
49
|
if (window.URLPattern) {
|
|
46
50
|
const urLPattern = new window.URLPattern({ pathname: pattern });
|
|
@@ -52,9 +56,9 @@ export function matchPath(pattern: string, path: string) {
|
|
|
52
56
|
const matchResult = path.match(reg) || `${path}/`.match(reg);
|
|
53
57
|
if (!matchResult) return null;
|
|
54
58
|
const params: Params = {};
|
|
55
|
-
if (matchResult) {
|
|
56
|
-
Object.
|
|
57
|
-
params[
|
|
59
|
+
if (matchResult.groups) {
|
|
60
|
+
Object.entries(matchResult.groups).forEach(([key, value]) => {
|
|
61
|
+
params[key.startsWith(UNNAMED_PARAM_PREFIX) ? key.slice(1) : key] = value;
|
|
58
62
|
});
|
|
59
63
|
}
|
|
60
64
|
return params;
|
|
@@ -111,29 +115,44 @@ type State = {
|
|
|
111
115
|
content?: TemplateResult;
|
|
112
116
|
};
|
|
113
117
|
|
|
118
|
+
export type RouteTrigger = {
|
|
119
|
+
store: Store<any>;
|
|
120
|
+
replace: (arg: { path: string }) => void;
|
|
121
|
+
getParams: () => { path: string; query?: string | QueryString; hash?: string };
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const scrollPositionMap = new Map<string, number>();
|
|
125
|
+
|
|
114
126
|
/**
|
|
115
127
|
* @attr inert 暂停路由更新
|
|
116
128
|
* @fires routechange
|
|
117
129
|
* @fires error
|
|
118
130
|
* @fires loading
|
|
119
131
|
*/
|
|
120
|
-
@connectStore(history.store)
|
|
121
132
|
export class GemRouteElement extends GemElement<State> {
|
|
122
133
|
@boolattribute transition: boolean;
|
|
123
134
|
@property routes?: RouteItem[] | RoutesObject;
|
|
124
135
|
/**
|
|
125
|
-
*
|
|
136
|
+
* 路由更新后更新的 store,能从中读取到安全的路由数据
|
|
126
137
|
*
|
|
127
138
|
* @example
|
|
128
139
|
* const locationStore = GemRouteElement.createLocationStore()
|
|
129
140
|
* html`<gem-route .locationStore=${locationStore}>`
|
|
130
141
|
*/
|
|
131
|
-
@property locationStore?:
|
|
142
|
+
@property locationStore?: ReturnType<(typeof GemRouteElement)['createLocationStore']>;
|
|
132
143
|
@property key?: any; // 除了 href 提供另外一种方式来更新,比如语言更新也需要刷新 <gem-route>
|
|
144
|
+
@emitter loading: Emitter<RouteItem | null>;
|
|
145
|
+
@emitter beforeroutechange: Emitter<RouteItem | null>;
|
|
133
146
|
@emitter routechange: Emitter<RouteItem | null>; // path 改变或者 key 改变,包含初始渲染
|
|
134
|
-
@emitter loading: Emitter<RouteItem>;
|
|
135
147
|
@emitter error: Emitter<any>;
|
|
136
148
|
|
|
149
|
+
/**自动让滚动容器恢复滚动位置 */
|
|
150
|
+
@property scrollContainer?: HTMLElement | (() => HTMLElement);
|
|
151
|
+
/**默认当有 `hash` 时不进行滚动 */
|
|
152
|
+
@property scrollIgnoreHash?: boolean;
|
|
153
|
+
|
|
154
|
+
@property trigger: RouteTrigger = history;
|
|
155
|
+
|
|
137
156
|
/**当前使用的路由对象 */
|
|
138
157
|
currentRoute: RouteItem | null;
|
|
139
158
|
/**当前匹配的路由的 params */
|
|
@@ -141,9 +160,10 @@ export class GemRouteElement extends GemElement<State> {
|
|
|
141
160
|
|
|
142
161
|
#lastLoader?: Promise<TemplateResult>;
|
|
143
162
|
|
|
163
|
+
/**不要多个 `<gem-route>` 共享,因为那样会导致后面的元素卸载前触发更新 */
|
|
144
164
|
static createLocationStore = () => {
|
|
145
|
-
const { path, query, data } = history.getParams();
|
|
146
|
-
return createStore({ path, query, data, params: {} as
|
|
165
|
+
const { path, query, hash, data } = history.getParams();
|
|
166
|
+
return createStore({ path, query, hash, data, params: {} as Params });
|
|
147
167
|
};
|
|
148
168
|
|
|
149
169
|
static findRoute = (target: RouteItem[] | RoutesObject = [], path: string) => {
|
|
@@ -167,6 +187,10 @@ export class GemRouteElement extends GemElement<State> {
|
|
|
167
187
|
return { route: defaultRoute };
|
|
168
188
|
};
|
|
169
189
|
|
|
190
|
+
get #scrollContainer() {
|
|
191
|
+
return typeof this.scrollContainer === 'function' ? this.scrollContainer() : this.scrollContainer;
|
|
192
|
+
}
|
|
193
|
+
|
|
170
194
|
constructor({ isLight, routes }: ConstructorOptions = {}) {
|
|
171
195
|
super({ isLight });
|
|
172
196
|
this.routes = routes;
|
|
@@ -177,38 +201,65 @@ export class GemRouteElement extends GemElement<State> {
|
|
|
177
201
|
};
|
|
178
202
|
|
|
179
203
|
#updateLocationStore = () => {
|
|
180
|
-
if (this.locationStore)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
204
|
+
if (!this.locationStore) return;
|
|
205
|
+
const { path, query, hash, data } = history.getParams();
|
|
206
|
+
updateStore(this.locationStore, {
|
|
207
|
+
path,
|
|
208
|
+
params: this.currentParams,
|
|
209
|
+
query,
|
|
210
|
+
hash,
|
|
211
|
+
data,
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
#updateScrollContainerPosition = () => {
|
|
216
|
+
if (!this.#scrollContainer) return;
|
|
217
|
+
const scrollTop = scrollPositionMap.get(history.currentKey);
|
|
218
|
+
if (scrollTop) {
|
|
219
|
+
this.#scrollContainer.scroll(0, scrollTop);
|
|
220
|
+
} else if (this.scrollIgnoreHash || !history.getParams().hash) {
|
|
221
|
+
this.#scrollContainer.scroll(0, 0);
|
|
188
222
|
}
|
|
189
223
|
};
|
|
190
224
|
|
|
191
225
|
#setContent = (route: RouteItem | null, params: Params, content?: TemplateResult) => {
|
|
226
|
+
this.beforeroutechange(route);
|
|
192
227
|
this.#lastLoader = undefined;
|
|
193
228
|
this.currentRoute = route;
|
|
194
229
|
this.currentParams = params;
|
|
195
|
-
const changeContent = () => {
|
|
230
|
+
const changeContent = async () => {
|
|
196
231
|
this.setState({ content });
|
|
197
|
-
|
|
232
|
+
// 要确保新页面能读取到新路由数据
|
|
198
233
|
this.#updateLocationStore();
|
|
234
|
+
// 等待页面渲染完成
|
|
235
|
+
await Promise.resolve();
|
|
236
|
+
this.#updateScrollContainerPosition();
|
|
237
|
+
this.routechange(this.currentRoute);
|
|
199
238
|
};
|
|
200
239
|
if (this.transition && 'startViewTransition' in document) {
|
|
201
|
-
(document as any).startViewTransition(() =>
|
|
202
|
-
changeContent();
|
|
203
|
-
// 等待路由渲染
|
|
204
|
-
return Promise.resolve();
|
|
205
|
-
});
|
|
240
|
+
(document as any).startViewTransition(() => changeContent());
|
|
206
241
|
} else {
|
|
207
242
|
changeContent();
|
|
208
243
|
}
|
|
209
244
|
};
|
|
210
245
|
|
|
211
246
|
mounted() {
|
|
247
|
+
this.effect(
|
|
248
|
+
() => {
|
|
249
|
+
if (!this.#scrollContainer) return;
|
|
250
|
+
return addListener(history, 'beforechange', () =>
|
|
251
|
+
scrollPositionMap.set(history.currentKey, this.#scrollContainer!.scrollTop),
|
|
252
|
+
);
|
|
253
|
+
},
|
|
254
|
+
() => [this.scrollContainer],
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
this.effect(
|
|
258
|
+
// 触发 this.#update
|
|
259
|
+
() => connect(this.trigger.store, () => this.setState({})),
|
|
260
|
+
() => [this.trigger],
|
|
261
|
+
);
|
|
262
|
+
|
|
212
263
|
this.effect(
|
|
213
264
|
([key, path], old) => {
|
|
214
265
|
// 只有查询参数改变
|
|
@@ -216,40 +267,17 @@ export class GemRouteElement extends GemElement<State> {
|
|
|
216
267
|
this.#updateLocationStore();
|
|
217
268
|
return;
|
|
218
269
|
}
|
|
219
|
-
|
|
220
|
-
const { redirect, content, getContent } = route || {};
|
|
221
|
-
if (redirect) {
|
|
222
|
-
history.replace({ path: redirect });
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
const title = route?.title;
|
|
226
|
-
if (title) updateStore(titleStore, { title });
|
|
227
|
-
const contentOrLoader = content || getContent?.(params);
|
|
228
|
-
if (contentOrLoader instanceof Promise) {
|
|
229
|
-
this.loading(route!);
|
|
230
|
-
this.#lastLoader = contentOrLoader;
|
|
231
|
-
const isSomeLoader = () => this.#lastLoader === contentOrLoader;
|
|
232
|
-
contentOrLoader
|
|
233
|
-
.then((content) => {
|
|
234
|
-
if (isSomeLoader()) this.#setContent(route, params, content);
|
|
235
|
-
})
|
|
236
|
-
.catch((err) => {
|
|
237
|
-
if (isSomeLoader()) this.error(err);
|
|
238
|
-
});
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
this.#setContent(route, params, contentOrLoader);
|
|
270
|
+
this.update();
|
|
242
271
|
},
|
|
243
272
|
() => {
|
|
244
|
-
const { path } =
|
|
245
|
-
return [this.key, path,
|
|
273
|
+
const { path, query, hash } = this.trigger.getParams();
|
|
274
|
+
return [this.key, path, query, hash] as const;
|
|
246
275
|
},
|
|
247
276
|
);
|
|
248
277
|
}
|
|
249
278
|
|
|
250
279
|
shouldUpdate() {
|
|
251
|
-
|
|
252
|
-
return true;
|
|
280
|
+
return this.inert ? false : true;
|
|
253
281
|
}
|
|
254
282
|
|
|
255
283
|
render() {
|
|
@@ -266,6 +294,35 @@ export class GemRouteElement extends GemElement<State> {
|
|
|
266
294
|
${content ?? html`<slot></slot>`}
|
|
267
295
|
`;
|
|
268
296
|
}
|
|
297
|
+
|
|
298
|
+
update = () => {
|
|
299
|
+
const { route, params = {} } = GemRouteElement.findRoute(this.routes, this.trigger.getParams().path);
|
|
300
|
+
const { redirect, content, getContent } = route || {};
|
|
301
|
+
if (redirect) {
|
|
302
|
+
this.trigger.replace({
|
|
303
|
+
path: createPath({ pattern: redirect }, { params }),
|
|
304
|
+
});
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (this.trigger === history) {
|
|
308
|
+
updateStore(titleStore, { title: route?.title });
|
|
309
|
+
}
|
|
310
|
+
const contentOrLoader = content || getContent?.(params);
|
|
311
|
+
this.loading(route);
|
|
312
|
+
if (contentOrLoader instanceof Promise) {
|
|
313
|
+
this.#lastLoader = contentOrLoader;
|
|
314
|
+
const isSomeLoader = () => this.#lastLoader === contentOrLoader;
|
|
315
|
+
contentOrLoader
|
|
316
|
+
.then((content) => {
|
|
317
|
+
if (isSomeLoader()) this.#setContent(route, params, content);
|
|
318
|
+
})
|
|
319
|
+
.catch((err) => {
|
|
320
|
+
if (isSomeLoader()) this.error(err);
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.#setContent(route, params, contentOrLoader);
|
|
325
|
+
};
|
|
269
326
|
}
|
|
270
327
|
|
|
271
328
|
export class GemLightRouteElement extends GemRouteElement {
|
|
@@ -17,18 +17,24 @@ import { GemElement, html } from '../../lib/element';
|
|
|
17
17
|
import { attribute, boolattribute } from '../../lib/decorators';
|
|
18
18
|
import { updateStore, connect } from '../../lib/store';
|
|
19
19
|
import { titleStore } from '../../lib/history';
|
|
20
|
+
import { addMicrotask } from '../../lib/utils';
|
|
20
21
|
|
|
21
22
|
const defaultTitle = document.title;
|
|
22
23
|
|
|
24
|
+
let documentTitle = '';
|
|
25
|
+
const setTitle = () => (document.title = documentTitle);
|
|
26
|
+
|
|
23
27
|
function setDocumentTitle(str?: string | null, prefix = '', suffix = '') {
|
|
24
28
|
const title = titleStore.title || str;
|
|
25
29
|
if (title && title !== defaultTitle) {
|
|
26
30
|
GemTitleElement.title = title;
|
|
27
|
-
|
|
31
|
+
documentTitle = prefix + GemTitleElement.title + suffix;
|
|
28
32
|
} else {
|
|
29
33
|
GemTitleElement.title = defaultTitle;
|
|
30
|
-
|
|
34
|
+
documentTitle = GemTitleElement.title;
|
|
31
35
|
}
|
|
36
|
+
// 避免重定向时的中间状态
|
|
37
|
+
addMicrotask(setTitle);
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
connect(titleStore, setDocumentTitle);
|
|
@@ -62,6 +68,7 @@ export class GemTitleElement extends GemElement {
|
|
|
62
68
|
super();
|
|
63
69
|
new MutationObserver(() => this.update()).observe(this, {
|
|
64
70
|
characterData: true,
|
|
71
|
+
childList: true,
|
|
65
72
|
subtree: true,
|
|
66
73
|
});
|
|
67
74
|
}
|
package/src/helper/i18n.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createStore, updateStore, Store } from '../lib/store';
|
|
2
2
|
import { html, TemplateResult } from '../lib/element';
|
|
3
3
|
import { GemError } from '../lib/utils';
|
|
4
|
+
import type { RouteTrigger } from '../elements/base/route';
|
|
5
|
+
|
|
6
|
+
import { logger } from './logger';
|
|
4
7
|
|
|
5
8
|
// Hello, $1
|
|
6
9
|
// See $1<detail>
|
|
@@ -59,7 +62,7 @@ interface I18nOptions<T> {
|
|
|
59
62
|
onChange?: (currentLanguage: string) => void;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
export class I18n<T = Record<string, Msg>> {
|
|
65
|
+
export class I18n<T = Record<string, Msg>> implements RouteTrigger {
|
|
63
66
|
resources: Resources<T>;
|
|
64
67
|
fallbackLanguage: string;
|
|
65
68
|
currentLanguage: string;
|
|
@@ -71,6 +74,12 @@ export class I18n<T = Record<string, Msg>> {
|
|
|
71
74
|
|
|
72
75
|
store: Store<any>;
|
|
73
76
|
|
|
77
|
+
replace = ({ path }: { path: string }) => {
|
|
78
|
+
this.setLanguage(path);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
getParams = () => ({ path: this.currentLanguage });
|
|
82
|
+
|
|
74
83
|
get #cacheCurrentKey() {
|
|
75
84
|
return `${this.cachePrefix}:current`;
|
|
76
85
|
}
|
|
@@ -164,8 +173,7 @@ export class I18n<T = Record<string, Msg>> {
|
|
|
164
173
|
let pack: Partial<T>;
|
|
165
174
|
const data = this.resources[lang];
|
|
166
175
|
if (!data) {
|
|
167
|
-
|
|
168
|
-
console.warn(`${this.cachePrefix}: not found \`${lang}\``);
|
|
176
|
+
logger.warn(`${this.cachePrefix}: not found \`${lang}\``);
|
|
169
177
|
return this.resetLanguage();
|
|
170
178
|
}
|
|
171
179
|
|
|
@@ -202,7 +210,7 @@ export class I18n<T = Record<string, Msg>> {
|
|
|
202
210
|
pack = data;
|
|
203
211
|
}
|
|
204
212
|
this.resources[lang] = pack;
|
|
205
|
-
updateStore(this.store
|
|
213
|
+
updateStore(this.store);
|
|
206
214
|
if (lang !== this.currentLanguage) {
|
|
207
215
|
this.#lang = lang;
|
|
208
216
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class Logger {
|
|
2
|
+
_type: string;
|
|
3
|
+
|
|
4
|
+
constructor(type: string) {
|
|
5
|
+
this._type = type;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
info = (...args: any[]) => {
|
|
9
|
+
console.log(`[${this._type}]:`, ...args);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
warn = (...args: any[]) => {
|
|
13
|
+
console.warn(`[${this._type}]:`, ...args);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
error = (...args: any[]) => {
|
|
17
|
+
console.error(`[${this._type}]:`, ...args);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const logger = new Logger('GEM');
|
package/src/helper/ssr-shim.ts
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
// @ts-nocheck
|
|
3
3
|
if (typeof window === 'undefined') {
|
|
4
4
|
const getAny = () => {
|
|
5
|
-
|
|
5
|
+
// constructable
|
|
6
|
+
function anonymous() {
|
|
7
|
+
return getAny();
|
|
8
|
+
}
|
|
6
9
|
Object.defineProperties(anonymous, {
|
|
7
10
|
length: { writable: true },
|
|
8
11
|
name: { writable: true },
|
|
@@ -16,14 +19,21 @@ if (typeof window === 'undefined') {
|
|
|
16
19
|
};
|
|
17
20
|
const any = getAny();
|
|
18
21
|
|
|
22
|
+
globalThis.window = globalThis;
|
|
23
|
+
globalThis.location = new URL('about:blank');
|
|
24
|
+
globalThis.HTMLElement = null;
|
|
25
|
+
|
|
19
26
|
globalThis.document = any;
|
|
20
27
|
globalThis.addEventListener = any;
|
|
21
28
|
globalThis.history = any;
|
|
22
29
|
globalThis.navigator = any;
|
|
23
30
|
globalThis.customElements = any;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
globalThis.
|
|
31
|
+
|
|
32
|
+
// duoyun-ui
|
|
33
|
+
globalThis.CSSStyleSheet = any;
|
|
34
|
+
globalThis.Document = any;
|
|
35
|
+
globalThis.Element = any;
|
|
36
|
+
globalThis.MutationObserver = any;
|
|
27
37
|
}
|
|
28
38
|
|
|
29
39
|
export {};
|
package/src/lib/element.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
kebabToCamelCase,
|
|
14
14
|
PropProxyMap,
|
|
15
15
|
removeItems,
|
|
16
|
+
addListener,
|
|
16
17
|
} from './utils';
|
|
17
18
|
import * as GemExports from './element';
|
|
18
19
|
import * as VersionExports from './version';
|
|
@@ -75,12 +76,10 @@ const constructorSymbol = Symbol('constructor');
|
|
|
75
76
|
const initSymbol = Symbol('init');
|
|
76
77
|
const updateSymbol = Symbol('update');
|
|
77
78
|
|
|
78
|
-
export interface GemElementOptions {
|
|
79
|
+
export interface GemElementOptions extends Partial<ShadowRootInit> {
|
|
79
80
|
isLight?: boolean;
|
|
80
81
|
isAsync?: boolean;
|
|
81
|
-
|
|
82
|
-
delegatesFocus?: boolean;
|
|
83
|
-
slotAssignment?: 'named' | 'manual';
|
|
82
|
+
focusable?: boolean;
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
export abstract class GemElement<T = Record<string, unknown>> extends HTMLElement {
|
|
@@ -117,7 +116,7 @@ export abstract class GemElement<T = Record<string, unknown>> extends HTMLElemen
|
|
|
117
116
|
#memoList?: EffectItem<any>[];
|
|
118
117
|
#unmountCallback?: any;
|
|
119
118
|
|
|
120
|
-
constructor(
|
|
119
|
+
constructor(options: GemElementOptions = {}) {
|
|
121
120
|
super();
|
|
122
121
|
|
|
123
122
|
// 外部不可见,但允许类外面使用
|
|
@@ -130,8 +129,37 @@ export abstract class GemElement<T = Record<string, unknown>> extends HTMLElemen
|
|
|
130
129
|
}
|
|
131
130
|
});
|
|
132
131
|
|
|
133
|
-
this.#isAsync = isAsync;
|
|
134
|
-
this.#renderRoot = isLight
|
|
132
|
+
this.#isAsync = options.isAsync;
|
|
133
|
+
this.#renderRoot = options.isLight
|
|
134
|
+
? this
|
|
135
|
+
: this.attachShadow({
|
|
136
|
+
mode: options.mode || 'open',
|
|
137
|
+
delegatesFocus: options.delegatesFocus,
|
|
138
|
+
slotAssignment: options.slotAssignment,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// https://stackoverflow.com/questions/43836886/failed-to-construct-customelement-error-when-javascript-file-is-placed-in-head
|
|
142
|
+
// focusable 元素一般同时具备 disabled 属性
|
|
143
|
+
// 和原生元素行为保持一致,disabled 时不触发 click 事件
|
|
144
|
+
let hasInitTabIndex: boolean | undefined;
|
|
145
|
+
this.effect(
|
|
146
|
+
([disabled = false]) => {
|
|
147
|
+
if (hasInitTabIndex === undefined) hasInitTabIndex = this.hasAttribute('tabindex');
|
|
148
|
+
|
|
149
|
+
this.internals.ariaDisabled = String(disabled);
|
|
150
|
+
|
|
151
|
+
if (options.focusable && !hasInitTabIndex) {
|
|
152
|
+
this.tabIndex = -Number(disabled);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if ((options.focusable || options.delegatesFocus) && disabled) {
|
|
156
|
+
return addListener(this, 'click', (e: Event) => e.isTrusted && e.stopImmediatePropagation(), {
|
|
157
|
+
capture: true,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
() => [Reflect.get(this, 'disabled')],
|
|
162
|
+
);
|
|
135
163
|
|
|
136
164
|
const { adoptedStyleSheets } = new.target;
|
|
137
165
|
if (adoptedStyleSheets) {
|
|
@@ -248,7 +276,8 @@ export abstract class GemElement<T = Record<string, unknown>> extends HTMLElemen
|
|
|
248
276
|
|
|
249
277
|
/**
|
|
250
278
|
* @helper
|
|
251
|
-
* 在 `render`
|
|
279
|
+
* 在 `render` 前执行回调,不要在里面使用 `setState`
|
|
280
|
+
* 和 `effect` 一样接受依赖数组参数,在 `constructor`/`willMount` 中使用;
|
|
252
281
|
* 第一次执行时 `oldDeps` 为空
|
|
253
282
|
*
|
|
254
283
|
* ```js
|
|
@@ -444,15 +473,17 @@ export abstract class GemElement<T = Record<string, unknown>> extends HTMLElemen
|
|
|
444
473
|
this.#disconnectStore?.forEach((disconnect) => disconnect());
|
|
445
474
|
execCallback(this.#unmountCallback);
|
|
446
475
|
this.unmounted?.();
|
|
447
|
-
this.#effectList
|
|
448
|
-
this.#
|
|
449
|
-
this.#memoList = this.#memoList?.filter(this.#filterEffect);
|
|
476
|
+
this.#effectList = this.#clearEffect(this.#effectList);
|
|
477
|
+
this.#memoList = this.#clearEffect(this.#memoList);
|
|
450
478
|
return GemElement.#final;
|
|
451
479
|
}
|
|
452
480
|
|
|
453
|
-
#
|
|
454
|
-
e
|
|
455
|
-
|
|
481
|
+
#clearEffect = (list?: EffectItem<any>[]) => {
|
|
482
|
+
return list?.filter((e) => {
|
|
483
|
+
execCallback(e.preCallback);
|
|
484
|
+
e.initialized = false;
|
|
485
|
+
return e.inConstructor;
|
|
486
|
+
});
|
|
456
487
|
};
|
|
457
488
|
}
|
|
458
489
|
|
|
@@ -555,9 +586,7 @@ export function defineRef(target: GemElement, prop: string, ref: string) {
|
|
|
555
586
|
return ref;
|
|
556
587
|
},
|
|
557
588
|
get element() {
|
|
558
|
-
const gemReflects =
|
|
559
|
-
(e) => e.target,
|
|
560
|
-
);
|
|
589
|
+
const gemReflects = [...ele.querySelectorAll<GemReflectElement>('[data-gem-reflect]')].map((e) => e.target);
|
|
561
590
|
for (const e of [ele, ...gemReflects]) {
|
|
562
591
|
const result = e.querySelector(`[ref=${ref}]`);
|
|
563
592
|
if (result) return result;
|
package/src/lib/history.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface UpdateHistoryParams {
|
|
|
29
29
|
title?: string;
|
|
30
30
|
path?: string; // 不包括 basePath,只有根目录储存末尾斜杠
|
|
31
31
|
query?: string | QueryString; // 包含 `?`
|
|
32
|
-
hash?: string; // 包含
|
|
32
|
+
hash?: string; // 包含 `#`,不进行编码,方便比较
|
|
33
33
|
// 以下为看不见的状态
|
|
34
34
|
open?: () => void; // 按下前进键时执行
|
|
35
35
|
close?: () => void; // 按下返回键时执行
|
|
@@ -38,7 +38,12 @@ export interface UpdateHistoryParams {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// 实际应用值
|
|
41
|
-
type HistoryParams = UpdateHistoryParams & {
|
|
41
|
+
type HistoryParams = Omit<UpdateHistoryParams, 'query'> & {
|
|
42
|
+
title: string;
|
|
43
|
+
path: string;
|
|
44
|
+
query: QueryString;
|
|
45
|
+
hash: string;
|
|
46
|
+
};
|
|
42
47
|
|
|
43
48
|
const paramsMap = new Map<string, HistoryParams>();
|
|
44
49
|
|
|
@@ -60,19 +65,26 @@ function getInternalPath(urlBarPath: string) {
|
|
|
60
65
|
return urlBarPath.replace(new RegExp(`^${gemHistory.basePath}/`), '/');
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
function dispatchBeforeChangeEvent() {
|
|
69
|
+
gemHistory.dispatchEvent(new CustomEvent('beforechange'));
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
function normalizeParams(params: UpdateHistoryParams): HistoryParams {
|
|
64
|
-
const current =
|
|
73
|
+
const current =
|
|
74
|
+
paramsMap.get(store.$key) ||
|
|
75
|
+
({ path: getInternalPath(location.pathname), query: new QueryString() } as HistoryParams);
|
|
65
76
|
// 没提供 path 使用当前 path
|
|
66
77
|
const path = params.path ? absoluteLocation(current.path, params.path) : getInternalPath(location.pathname);
|
|
67
78
|
// 没提供 query 又没有提供 path 时使用当前 search
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
79
|
+
const newQueryObject = new QueryString(params.query ?? (params.path ? '' : location.search));
|
|
80
|
+
const queryChanged = String(newQueryObject) !== String(current.query);
|
|
81
|
+
const query = queryChanged ? newQueryObject : current.query;
|
|
82
|
+
const urlChanged = path !== current.path || queryChanged;
|
|
71
83
|
// 没提供 title 又没有改变 URL 时使用当前 document.title
|
|
72
|
-
const title = params.title || (
|
|
84
|
+
const title = params.title || (urlChanged ? '' : document.title);
|
|
73
85
|
const statusChanged = params.close || params.data || params.open || params.shouldClose;
|
|
74
|
-
// 没提供 hash 又没有改变 URL
|
|
75
|
-
const hash = params.hash
|
|
86
|
+
// 没提供 hash 又没有改变 URL 仅仅是状态更新时使用当前 hash
|
|
87
|
+
const hash = decodeURIComponent(params.hash ?? (!urlChanged && statusChanged ? location.hash : ''));
|
|
76
88
|
return { ...params, title, path, query, hash };
|
|
77
89
|
}
|
|
78
90
|
|
|
@@ -89,6 +101,7 @@ function updateHistory(p: UpdateHistoryParams, native: typeof nativeHistory.push
|
|
|
89
101
|
...data,
|
|
90
102
|
};
|
|
91
103
|
paramsMap.set(state.$key, params);
|
|
104
|
+
dispatchBeforeChangeEvent();
|
|
92
105
|
cleanObject(store);
|
|
93
106
|
updateHistoryStore(state);
|
|
94
107
|
const url = getUrlBarPath(path) + new QueryString(query) + hash;
|
|
@@ -108,6 +121,7 @@ function updateHistoryByNative(data: any, title: string, originUrl: string, nati
|
|
|
108
121
|
const { pathname, search, hash } = new URL(originUrl, location.origin + location.pathname);
|
|
109
122
|
const params = normalizeParams({ path: pathname, query: new QueryString(search), hash, title, data });
|
|
110
123
|
paramsMap.set(state.$key, params);
|
|
124
|
+
dispatchBeforeChangeEvent();
|
|
111
125
|
cleanObject(store);
|
|
112
126
|
updateHistoryStore(state);
|
|
113
127
|
const url = getUrlBarPath(pathname) + params.query + hash;
|
|
@@ -121,10 +135,13 @@ const [gemBasePathStore, updateBasePathStore] = useStore({
|
|
|
121
135
|
basePath: '',
|
|
122
136
|
});
|
|
123
137
|
|
|
124
|
-
class GemHistory {
|
|
138
|
+
class GemHistory extends EventTarget {
|
|
125
139
|
get store() {
|
|
126
140
|
return store;
|
|
127
141
|
}
|
|
142
|
+
get currentKey() {
|
|
143
|
+
return store.$key;
|
|
144
|
+
}
|
|
128
145
|
get basePath() {
|
|
129
146
|
return gemBasePathStore.basePath;
|
|
130
147
|
}
|
|
@@ -190,7 +207,7 @@ if (!window._GEMHISTORY) {
|
|
|
190
207
|
// 点击 `<a>`
|
|
191
208
|
window.addEventListener('hashchange', ({ isTrusted }) => {
|
|
192
209
|
if (isTrusted) {
|
|
193
|
-
gemHistory.replace({ hash:
|
|
210
|
+
gemHistory.replace({ hash: location.hash });
|
|
194
211
|
}
|
|
195
212
|
});
|
|
196
213
|
|
|
@@ -254,7 +271,7 @@ if (!window._GEMHISTORY) {
|
|
|
254
271
|
paramsMap.set(newState.$key, {
|
|
255
272
|
path: pathname,
|
|
256
273
|
query: new QueryString(search),
|
|
257
|
-
hash,
|
|
274
|
+
hash: decodeURIComponent(hash),
|
|
258
275
|
title: newState.$title, // document.title 是导航前的
|
|
259
276
|
data: newState,
|
|
260
277
|
});
|
|
@@ -292,6 +309,7 @@ if (!window._GEMHISTORY) {
|
|
|
292
309
|
}
|
|
293
310
|
}
|
|
294
311
|
|
|
312
|
+
dispatchBeforeChangeEvent();
|
|
295
313
|
cleanObject(store);
|
|
296
314
|
updateHistoryStore(newState);
|
|
297
315
|
});
|