@mantou/gem 1.3.6 → 1.5.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.
Files changed (48) hide show
  1. package/elements/link.d.ts.map +1 -1
  2. package/elements/link.js +2 -2
  3. package/elements/link.js.map +1 -1
  4. package/elements/modal-base.d.ts +86 -31
  5. package/elements/modal-base.d.ts.map +1 -1
  6. package/elements/reflect.d.ts.map +1 -1
  7. package/elements/reflect.js +8 -1
  8. package/elements/reflect.js.map +1 -1
  9. package/elements/route.d.ts +43 -12
  10. package/elements/route.d.ts.map +1 -1
  11. package/elements/route.js +114 -93
  12. package/elements/route.js.map +1 -1
  13. package/elements/use.d.ts.map +1 -1
  14. package/elements/use.js +12 -6
  15. package/elements/use.js.map +1 -1
  16. package/helper/i18n.d.ts +1 -1
  17. package/helper/i18n.d.ts.map +1 -1
  18. package/helper/i18n.js +7 -7
  19. package/helper/i18n.js.map +1 -1
  20. package/lib/element.d.ts +25 -5
  21. package/lib/element.d.ts.map +1 -1
  22. package/lib/element.js +49 -13
  23. package/lib/element.js.map +1 -1
  24. package/lib/history.js +1 -1
  25. package/lib/history.js.map +1 -1
  26. package/lib/utils.d.ts +6 -1
  27. package/lib/utils.d.ts.map +1 -1
  28. package/lib/utils.js +39 -1
  29. package/lib/utils.js.map +1 -1
  30. package/lib/version.d.ts +1 -1
  31. package/lib/version.js +1 -1
  32. package/package.json +1 -1
  33. package/src/elements/link.ts +3 -3
  34. package/src/elements/reflect.ts +9 -1
  35. package/src/elements/route.ts +125 -105
  36. package/src/elements/use.ts +11 -5
  37. package/src/examples/elements/layout.ts +2 -1
  38. package/src/examples/ref-route/index.ts +73 -40
  39. package/src/examples/svg-icon/index.ts +12 -1
  40. package/src/helper/i18n.ts +9 -7
  41. package/src/lib/element.ts +54 -9
  42. package/src/lib/history.ts +1 -1
  43. package/src/lib/utils.ts +36 -2
  44. package/src/lib/version.ts +1 -1
  45. package/src/test/gem-element/advance.test.ts +47 -0
  46. package/src/test/utils.test.ts +6 -0
  47. package/umd.js +13 -14
  48. package/umd.js.map +1 -1
@@ -11,13 +11,16 @@ import {
11
11
  UpdateHistoryParams,
12
12
  titleStore,
13
13
  updateStore,
14
- addMicrotaskToStack,
14
+ Store,
15
+ QueryString,
16
+ createStore,
15
17
  } from '../';
16
18
 
17
19
  interface NamePostition {
18
20
  [index: string]: number;
19
21
  }
20
22
 
23
+ // TODO: use `URLPattern`
21
24
  class ParamsRegExp extends RegExp {
22
25
  namePosition: NamePostition;
23
26
  constructor(pattern: string) {
@@ -36,15 +39,15 @@ class ParamsRegExp extends RegExp {
36
39
  }
37
40
  }
38
41
 
39
- function getReg(pattern: string) {
40
- return new ParamsRegExp(pattern);
41
- }
42
+ type Params = Record<string, string>;
42
43
 
43
44
  // `/a/b/:c/:d` `/a/b/1/2`
44
- function getParams(pattern: string, path: string) {
45
- const reg = getReg(pattern);
46
- const matchResult = path.match(reg);
47
- const params: Record<string, string> = {};
45
+ // 匹配成功时返回 params
46
+ export function matchPath(pattern: string, path: string) {
47
+ const reg = new ParamsRegExp(pattern);
48
+ const matchResult = path.match(reg) || `${path}/`.match(reg);
49
+ if (!matchResult) return null;
50
+ const params: Params = {};
48
51
  if (matchResult) {
49
52
  Object.keys(reg.namePosition).forEach((name) => {
50
53
  params[name] = matchResult[reg.namePosition[name] + 1];
@@ -53,10 +56,6 @@ function getParams(pattern: string, path: string) {
53
56
  return params;
54
57
  }
55
58
 
56
- export function isMatch(pattern: string, path: string) {
57
- return !!path.match(getReg(pattern)) || !!`${path}/`.match(getReg(pattern));
58
- }
59
-
60
59
  export interface RouteItem<T = unknown> {
61
60
  // 支持 *
62
61
  // 支持 /a/*
@@ -64,17 +63,17 @@ export interface RouteItem<T = unknown> {
64
63
  pattern: string;
65
64
  redirect?: string;
66
65
  content?: TemplateResult;
67
- getContent?: (params: Record<string, string>) => TemplateResult;
66
+ getContent?: (params: Params) => TemplateResult | Promise<TemplateResult>;
68
67
  title?: string;
69
68
  // 用来传递数据
70
69
  data?: T;
71
70
  }
72
71
 
73
- export type RoutesObject = Record<string, RouteItem>;
72
+ type RoutesObject = Record<string, RouteItem>;
74
73
 
75
74
  // params 中的成员不会验证
76
75
  export interface RouteOptions extends Omit<UpdateHistoryParams, 'path'> {
77
- params?: { [index: string]: string };
76
+ params?: Params;
78
77
  }
79
78
 
80
79
  // 从路由创建路径
@@ -103,127 +102,148 @@ interface ConstructorOptions {
103
102
  routes?: RouteItem[] | RoutesObject;
104
103
  }
105
104
 
105
+ type State = {
106
+ content?: TemplateResult;
107
+ };
108
+
106
109
  /**
107
110
  * @customElement gem-route
111
+ * @attr inert 暂停路由更新
108
112
  * @fires change
113
+ * @fires error
114
+ * @fires loading
109
115
  */
110
116
  @connectStore(history.store)
111
117
  @customElement('gem-route')
112
- export class GemRouteElement extends GemElement {
118
+ export class GemRouteElement extends GemElement<State> {
113
119
  @property routes?: RouteItem[] | RoutesObject;
120
+ /**
121
+ * @example
122
+ * const locationStore = GemRouteElement.createLocationStore()
123
+ * html`<gem-route .locationStore=${locationStore}>`
124
+ */
125
+ @property locationStore?: Store<{ path: string; params: Params; query: QueryString; data?: any }>;
114
126
  @property key: any; // 除了 href 提供另外一种方式来更新,比如语言更新也需要刷新 <gem-route>
115
- @emitter change: Emitter<RouteItem | null>;
116
-
117
- #href: string; // 用于内部比较
118
- #key: any; // 用于内部比较
119
- #redirect: boolean;
120
- #isLight?: boolean;
127
+ @emitter change: Emitter<RouteItem | null>; // path 改变或者 key 改变,包含初始渲染
128
+ @emitter loading: Emitter<RouteItem>;
129
+ @emitter error: Emitter<any>;
121
130
 
122
131
  currentRoute: RouteItem | null;
132
+ // 当前匹配的路由的 params
133
+ currentParams: Params = {};
123
134
 
124
- // 获取当前匹配的路由的 params
125
- getParams() {
126
- return this.currentRoute ? getParams(this.currentRoute.pattern, history.getParams().path) : {};
127
- }
135
+ #lastLoader?: Promise<TemplateResult>;
136
+
137
+ static createLocationStore = () => {
138
+ const { path, query, data } = history.getParams();
139
+ return createStore({ path, query, data, params: {} as Record<string, string> });
140
+ };
141
+
142
+ static findRoute = (target: RouteItem[] | RoutesObject = [], path: string) => {
143
+ let defaultRoute: RouteItem | null = null;
144
+ let routes: RouteItem[];
145
+ if (target instanceof Array) {
146
+ routes = target;
147
+ } else {
148
+ routes = Object.values(target);
149
+ }
150
+ for (const route of routes) {
151
+ if ('*' === route.pattern) {
152
+ defaultRoute = route;
153
+ } else {
154
+ const params = matchPath(route.pattern, path);
155
+ if (params) {
156
+ return { route, params };
157
+ }
158
+ }
159
+ }
160
+ return { route: defaultRoute };
161
+ };
128
162
 
129
163
  constructor({ isLight, routes }: ConstructorOptions = {}) {
130
164
  super({ isLight });
131
- this.#isLight = isLight;
132
-
133
165
  this.routes = routes;
134
-
135
- const { path, query } = history.getParams();
136
- this.#href = path + query;
137
166
  }
138
167
 
139
- #initPage = () => {
140
- const title = this.currentRoute?.title;
141
- if (title) {
142
- updateStore(titleStore, { title });
168
+ state: State = {
169
+ content: undefined,
170
+ };
171
+
172
+ #updateLocationStore = () => {
173
+ if (this.locationStore) {
174
+ const { path, query, data } = history.getParams();
175
+ updateStore(this.locationStore, {
176
+ path,
177
+ params: this.currentParams,
178
+ query,
179
+ data,
180
+ });
143
181
  }
144
182
  };
145
183
 
146
- #callback = () => {
147
- this.currentRoute = null;
148
- return html``;
184
+ #setContent = (route: RouteItem | null, params: Params, content?: TemplateResult) => {
185
+ this.#lastLoader = undefined;
186
+ this.currentRoute = route;
187
+ this.currentParams = params;
188
+ this.setState({ content });
189
+ this.change(this.currentRoute);
190
+ const title = route?.title;
191
+ if (title) updateStore(titleStore, { title });
192
+ this.#updateLocationStore();
149
193
  };
150
194
 
195
+ mounted() {
196
+ this.effect(
197
+ ([_, path]) => {
198
+ const { route, params = {} } = GemRouteElement.findRoute(this.routes, path);
199
+ const { redirect, content, getContent } = route || {};
200
+ if (redirect) {
201
+ history.replace({ path: redirect });
202
+ return;
203
+ }
204
+ const contentOrLoader = content || getContent?.(params);
205
+ if (contentOrLoader instanceof Promise) {
206
+ this.loading(route!);
207
+ this.#lastLoader = contentOrLoader;
208
+ const isSomeLoader = () => this.#lastLoader === contentOrLoader;
209
+ contentOrLoader
210
+ .then((content) => {
211
+ if (isSomeLoader()) this.#setContent(route, params, content);
212
+ })
213
+ .catch((err) => {
214
+ if (isSomeLoader()) this.error(err);
215
+ });
216
+ return;
217
+ }
218
+ this.#setContent(route, params, contentOrLoader);
219
+ },
220
+ () => {
221
+ const { path } = history.getParams();
222
+ return [this.key, path];
223
+ },
224
+ );
225
+ this.effect(this.#updateLocationStore, () => [location.search]);
226
+ }
227
+
151
228
  shouldUpdate() {
152
229
  if (this.inert) return false;
153
- const { path, query } = history.getParams();
154
- const href = path + query;
155
- if (href !== this.#href || this.key !== this.#key) {
156
- this.#href = href;
157
- this.#key = this.key;
158
- return true;
159
- }
160
- return false;
230
+ return true;
161
231
  }
162
232
 
163
233
  render() {
164
- if (!this.routes) return this.#callback();
165
- const { path } = history.getParams();
166
- this.currentRoute = null;
167
-
168
- let defaultRoute: RouteItem | null = null;
169
- let routes: RouteItem[];
170
- if (this.routes instanceof Array) {
171
- routes = this.routes;
172
- } else {
173
- routes = Object.values(this.routes);
174
- }
175
-
176
- for (const item of routes) {
177
- const { pattern } = item;
178
- if ('*' === pattern) {
179
- defaultRoute = item;
180
- } else if (isMatch(pattern, path)) {
181
- this.currentRoute = item;
182
- break;
183
- }
184
- }
234
+ const { content } = this.state;
235
+ if (!this.shadowRoot) return html`${content}`;
185
236
 
186
- if (!this.currentRoute) {
187
- this.currentRoute = defaultRoute;
188
- }
189
-
190
- if (!this.currentRoute) return this.#callback();
191
-
192
- const { redirect, content, getContent } = this.currentRoute;
193
- if (redirect) {
194
- this.#redirect = true;
195
- history.replace({ path: redirect });
196
- // 不要渲染空内容,等待重定向结果
197
- return undefined;
198
- }
199
-
200
- this.#redirect = false;
201
237
  return html`
202
- ${this.#isLight
203
- ? ''
204
- : html`
205
- <style>
206
- :host,
207
- :not(:defined) {
208
- display: contents;
209
- }
210
- </style>
211
- `}
212
- ${content || getContent?.(this.getParams())}
238
+ <style>
239
+ :host,
240
+ :not(:defined) {
241
+ display: contents;
242
+ }
243
+ </style>
244
+ ${content}
213
245
  `;
214
246
  }
215
-
216
- mounted() {
217
- this.#key = this.key;
218
- addMicrotaskToStack(this.#initPage);
219
- }
220
-
221
- updated() {
222
- this.#initPage();
223
- if (!this.#redirect) {
224
- this.change(this.currentRoute);
225
- }
226
- }
227
247
  }
228
248
 
229
249
  /**
@@ -1,5 +1,6 @@
1
1
  import { GemElement, html, attribute, property, customElement } from '../';
2
2
 
3
+ const eleCache = new Map<string, HTMLTemplateElement>();
3
4
  /**
4
5
  * svg 中的 `<use>` 不能穿透 ShadowDOM,
5
6
  * 此元素用来模拟 `<use>`,
@@ -18,12 +19,16 @@ export class GemUseElement extends GemElement {
18
19
 
19
20
  private getContent() {
20
21
  const ele = this.element || (this.selector ? (this.root || document).querySelector(this.selector) : null);
21
- if (ele instanceof HTMLTemplateElement) {
22
+ if (typeof ele === 'string') {
23
+ let temp = eleCache.get(ele);
24
+ if (!temp) {
25
+ temp = document.createElement('template');
26
+ temp.innerHTML = ele;
27
+ eleCache.set(ele, temp);
28
+ }
29
+ return temp.content.cloneNode(true);
30
+ } else if (ele instanceof HTMLTemplateElement) {
22
31
  return ele.content.cloneNode(true);
23
- } else if (typeof ele === 'string') {
24
- const temp = document.createElement('template');
25
- temp.innerHTML = ele;
26
- return temp.content;
27
32
  } else {
28
33
  return ele?.cloneNode(true);
29
34
  }
@@ -37,6 +42,7 @@ export class GemUseElement extends GemElement {
37
42
  }
38
43
  :host(:not([hidden])) {
39
44
  display: inline-flex;
45
+ align-items: center;
40
46
  }
41
47
  svg {
42
48
  width: 100%;
@@ -18,7 +18,8 @@ export class Layout extends GemElement {
18
18
  <style>
19
19
  body {
20
20
  margin: 0;
21
- font-family: sans-serif;
21
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
22
+ sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
22
23
  }
23
24
  </style>
24
25
  </gem-reflect>
@@ -1,52 +1,77 @@
1
1
  import { GemElement, html, history, render } from '../../';
2
- import { customElement, RefObject, refobject } from '../../lib/decorators';
3
- import { createHistoryParams, GemRouteElement, RoutesObject } from '../../elements/route';
2
+ import { connectStore, customElement, RefObject, refobject } from '../../lib/decorators';
3
+ import { createHistoryParams, GemRouteElement, RouteItem } from '../../elements/route';
4
4
  import '../../elements/link';
5
5
 
6
6
  import '../elements/layout';
7
7
 
8
- const routes: RoutesObject = {
9
- home: {
10
- pattern: '/',
11
- get content() {
12
- return html`
13
- <style>
14
- gem-active-link {
15
- display: block;
16
- }
17
- gem-active-link:where(.--active, :--active) {
18
- color: inherit;
19
- }
20
- </style>
21
- current route: home page, click navigation to a page
22
- <gem-active-link .route=${routes.a} .options=${{ params: { b: 1 } }}>
23
- a page link, params: {a: 1}
24
- </gem-active-link>
25
- `;
26
- },
8
+ const homeRoute: RouteItem = {
9
+ pattern: '/',
10
+ getContent(params) {
11
+ return html`<route-home .params=${params}></route-home>`;
27
12
  },
28
- a: {
29
- pattern: '/a/:b',
30
- getContent({ b }) {
31
- return html`
32
- <style>
33
- gem-active-link {
34
- display: block;
35
- }
36
- gem-active-link:where(.--active, :--active) {
37
- color: inherit;
38
- }
39
- </style>
40
- current route: /a/${b}, click navigation to home page, cuurent query: ${history.getParams().query.toString()}
41
- <gem-active-link .route=${routes.a} .options=${{ params: { b: 1 }, query: '?a=1' }}>
42
- a page link, query: ?a=1
43
- </gem-active-link>
44
- `;
45
- },
13
+ };
14
+
15
+ const aRoute: RouteItem = {
16
+ pattern: '/a/:b',
17
+ async getContent() {
18
+ await new Promise((res) => setTimeout(res, 1000));
19
+ return html`<route-a></route-a>`;
46
20
  },
47
21
  };
48
22
 
23
+ const routes = {
24
+ home: homeRoute,
25
+ a: aRoute,
26
+ };
27
+
28
+ const locationStore = GemRouteElement.createLocationStore();
29
+
30
+ @customElement('route-home')
31
+ export class RouteHome extends GemElement {
32
+ params: Record<string, string>;
33
+ render = () => {
34
+ return html`
35
+ <style>
36
+ gem-active-link {
37
+ display: block;
38
+ }
39
+ gem-active-link:where(.--active, :--active) {
40
+ color: inherit;
41
+ }
42
+ </style>
43
+ current route: home page, current params: ${JSON.stringify(this.params)}, click navigation to /a page
44
+ <gem-active-link .route=${routes.a} .options=${{ params: { b: 1 } }}>
45
+ a page link, params: {a: 1}
46
+ </gem-active-link>
47
+ `;
48
+ };
49
+ }
50
+
51
+ @connectStore(locationStore)
52
+ @customElement('route-a')
53
+ export class RouteA extends GemElement {
54
+ render = () => {
55
+ return html`
56
+ <style>
57
+ gem-active-link {
58
+ display: block;
59
+ }
60
+ gem-active-link:where(.--active, :--active) {
61
+ color: inherit;
62
+ }
63
+ </style>
64
+ current route: /a/:b, current params: ${JSON.stringify(locationStore.params)}, click navigation to home page,
65
+ cuurent query: ${history.getParams().query.toString()}
66
+ <gem-active-link .route=${routes.a} .options=${{ params: { b: 1 }, query: '?a=1' }}>
67
+ a page link, query: ?a=1
68
+ </gem-active-link>
69
+ `;
70
+ };
71
+ }
72
+
49
73
  @customElement('app-root')
74
+ @connectStore(locationStore)
50
75
  export class App extends GemElement {
51
76
  @refobject routeRef: RefObject<GemRouteElement>;
52
77
  onclick = () => {
@@ -59,7 +84,15 @@ export class App extends GemElement {
59
84
  render() {
60
85
  return html`
61
86
  <main>
62
- <gem-route ref=${this.routeRef.ref} .routes=${routes}></gem-route>
87
+ <pre>${JSON.stringify(locationStore.params)}</pre>
88
+ <gem-route
89
+ ref=${this.routeRef.ref}
90
+ @loading=${console.log}
91
+ @change=${console.log}
92
+ @error=${console.error}
93
+ .routes=${routes}
94
+ .locationStore=${locationStore}
95
+ ></gem-route>
63
96
  </main>
64
97
  `;
65
98
  }
@@ -1,4 +1,4 @@
1
- import { html, render } from '../../';
1
+ import { html, render, raw } from '../../';
2
2
 
3
3
  import '../../elements/use';
4
4
 
@@ -14,6 +14,17 @@ const app = html`
14
14
  </template>
15
15
  <gem-use .root=${document.body} selector="#icon"></gem-use>
16
16
  <gem-use selector="#icon"></gem-use>
17
+ <gem-use
18
+ .element=${raw`
19
+ <svg height="24px" viewBox="0 0 24 24" width="24px">
20
+ <path d="M0 0h24v24H0z" fill="none"/>
21
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
22
+ </svg>
23
+ `}
24
+ style="fill: red; height: 1.2em; gap: 0.3em"
25
+ >
26
+ English
27
+ </gem-use>
17
28
  </div>
18
29
  `;
19
30
 
@@ -35,6 +35,8 @@ interface Options<T> {
35
35
  * 为了避免缓存超出 `localStorage` 限制,语言包 URL 应该带版本号查询参数
36
36
  */
37
37
  cache?: boolean | 'manual';
38
+ // lib 需要设置,避免被应用中的 i18n 一起清理掉语言缓存
39
+ // 也用来作为 prerender 的 script type
38
40
  cachePrefix?: string;
39
41
  // 为 `path` 时页面的路由不能和语言代码相同
40
42
  urlParamsType?: 'path' | 'querystring' | 'ccTLD' | 'gTLD';
@@ -47,7 +49,7 @@ export class I18n<T = Record<string, Msg>> {
47
49
  fallbackLanguage: string;
48
50
  currentLanguage: string;
49
51
  cache?: boolean | 'manual';
50
- cachePrefix = 'gem@i18n';
52
+ cachePrefix = 'gem-i18n';
51
53
  urlParamsType?: 'path' | 'querystring' | 'ccTLD' | 'gTLD';
52
54
  urlParamsName?: string;
53
55
  onChange?: (currentLanguage: string) => void;
@@ -94,7 +96,7 @@ export class I18n<T = Record<string, Msg>> {
94
96
 
95
97
  let currentLanguage = this.urlParamsLang || this.currentLanguage || this.cacheLang;
96
98
 
97
- const ele = document.querySelector('[type*=gem-i18n]');
99
+ const ele = document.querySelector(`[type*=${this.cachePrefix}]`);
98
100
  if (ele) {
99
101
  try {
100
102
  const prerenderData = JSON.parse(ele.textContent || '') as I18nJSON<T>;
@@ -152,17 +154,17 @@ export class I18n<T = Record<string, Msg>> {
152
154
  }
153
155
 
154
156
  if (typeof data === 'string') {
155
- const { href, origin, pathname } = new URL(data, location.href);
156
- const localKey = `${this.cachePrefix}:${href}`;
157
+ const prefix = `${this.cachePrefix}:`;
158
+ const localKey = `${prefix}${data}`;
157
159
  const localPackString = localStorage.getItem(localKey);
158
160
 
159
161
  const fetchPack = async () => {
160
162
  const pack = await (await fetch(data)).json();
161
163
  if (this.cache) {
162
- // 移除之前的版本缓存,只有使用 querystring 标记版本才有效
164
+ // 移除之前的版本缓存
163
165
  for (let i = 0; i < localStorage.length; i++) {
164
166
  const key = localStorage.key(i) as string;
165
- if (key.startsWith(`${this.cachePrefix}:${origin}${pathname}?`)) {
167
+ if (key.startsWith(prefix)) {
166
168
  localStorage.removeItem(key);
167
169
  }
168
170
  }
@@ -215,7 +217,7 @@ export class I18n<T = Record<string, Msg>> {
215
217
  *
216
218
  * html`
217
219
  * <gem-reflect>
218
- * <script type="gem-i18n">${JSON.stringify(i18n)}</script>
220
+ * <script type="${i18n.cachePrefix}">${JSON.stringify(i18n)}</script>
219
221
  * </gem-reflect>
220
222
  * `
221
223
  */