@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.
Files changed (69) hide show
  1. package/dist/gem.es.js +246 -113
  2. package/dist/gem.umd.js +24 -24
  3. package/elements/base/dialog.d.ts.map +1 -1
  4. package/elements/base/dialog.js +1 -1
  5. package/elements/base/dialog.js.map +1 -1
  6. package/elements/base/gesture.d.ts.map +1 -1
  7. package/elements/base/gesture.js +7 -1
  8. package/elements/base/gesture.js.map +1 -1
  9. package/elements/base/link.d.ts.map +1 -1
  10. package/elements/base/link.js +8 -4
  11. package/elements/base/link.js.map +1 -1
  12. package/elements/base/modal-factory.d.ts +28 -6
  13. package/elements/base/modal-factory.d.ts.map +1 -1
  14. package/elements/base/reflect.d.ts +8 -0
  15. package/elements/base/reflect.d.ts.map +1 -1
  16. package/elements/base/reflect.js +36 -9
  17. package/elements/base/reflect.js.map +1 -1
  18. package/elements/base/route.d.ts +38 -10
  19. package/elements/base/route.d.ts.map +1 -1
  20. package/elements/base/route.js +119 -73
  21. package/elements/base/route.js.map +1 -1
  22. package/elements/base/title.d.ts.map +1 -1
  23. package/elements/base/title.js +8 -2
  24. package/elements/base/title.js.map +1 -1
  25. package/helper/i18n.d.ts +8 -1
  26. package/helper/i18n.d.ts.map +1 -1
  27. package/helper/i18n.js +7 -3
  28. package/helper/i18n.js.map +1 -1
  29. package/helper/logger.d.ts +9 -0
  30. package/helper/logger.d.ts.map +1 -0
  31. package/helper/logger.js +16 -0
  32. package/helper/logger.js.map +1 -0
  33. package/helper/ssr-shim.d.ts.map +1 -1
  34. package/helper/ssr-shim.js +12 -4
  35. package/helper/ssr-shim.js.map +1 -1
  36. package/lib/element.d.ts +5 -5
  37. package/lib/element.d.ts.map +1 -1
  38. package/lib/element.js +40 -14
  39. package/lib/element.js.map +1 -1
  40. package/lib/history.d.ts +3 -2
  41. package/lib/history.d.ts.map +1 -1
  42. package/lib/history.js +22 -9
  43. package/lib/history.js.map +1 -1
  44. package/lib/utils.d.ts +2 -1
  45. package/lib/utils.d.ts.map +1 -1
  46. package/lib/utils.js +10 -8
  47. package/lib/utils.js.map +1 -1
  48. package/lib/version.d.ts +1 -1
  49. package/lib/version.d.ts.map +1 -1
  50. package/lib/version.js +1 -1
  51. package/lib/version.js.map +1 -1
  52. package/package.json +2 -2
  53. package/src/elements/base/dialog.ts +4 -1
  54. package/src/elements/base/gesture.ts +6 -1
  55. package/src/elements/base/link.ts +8 -4
  56. package/src/elements/base/reflect.ts +43 -9
  57. package/src/elements/base/route.ts +130 -73
  58. package/src/elements/base/title.ts +9 -2
  59. package/src/helper/i18n.ts +12 -4
  60. package/src/helper/logger.ts +21 -0
  61. package/src/helper/ssr-shim.ts +14 -4
  62. package/src/lib/element.ts +46 -17
  63. package/src/lib/history.ts +30 -12
  64. package/src/lib/utils.ts +16 -9
  65. package/src/lib/version.ts +1 -1
  66. package/src/test/elements/link.test.ts +15 -0
  67. package/src/test/elements/reflect.test.ts +27 -0
  68. package/src/test/elements/title.test.ts +25 -0
  69. package/src/test/utils.test.ts +14 -0
@@ -1,28 +1,25 @@
1
1
  import { GemElement, html, TemplateResult } from '../../lib/element';
2
- import { property, connectStore, emitter, Emitter, boolattribute } from '../../lib/decorators';
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
- interface NamePosition {
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
- .replace(/:([^/$]+)/g, (_m, name: string) => {
19
- namePosition[name] = i++;
20
- return `([^/]+)`;
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
- // URLPattern 大小写敏感
39
- // 匹配成功时返回 params
40
- // `/a/b/:c/:d` `/a/b/1/2`
41
- // pattern 以 / 结尾时能匹配 2 中路径
42
- // `/a/b/:c/:d/` `/a/b/1/2`
43
- // `/a/b/:c/:d/` `/a/b/1/2/`
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.keys(reg.namePosition).forEach((name) => {
57
- params[name] = matchResult[reg.namePosition[name] + 1];
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
- * 不要多个 `<gem-route>` 共享,因为那样会导致后面的元素卸载前触发更新
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?: Store<{ path: string; params: Params; query: QueryString; data?: any }>;
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 Record<string, string> });
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
- const { path, query, data } = history.getParams();
182
- updateStore(this.locationStore, {
183
- path,
184
- params: this.currentParams,
185
- query,
186
- data,
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
- this.routechange(this.currentRoute);
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
- const { route, params = {} } = GemRouteElement.findRoute(this.routes, path);
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 } = history.getParams();
245
- return [this.key, path, location.search];
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
- if (this.inert) return false;
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
- document.title = prefix + GemTitleElement.title + suffix;
31
+ documentTitle = prefix + GemTitleElement.title + suffix;
28
32
  } else {
29
33
  GemTitleElement.title = defaultTitle;
30
- document.title = GemTitleElement.title;
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
  }
@@ -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
- // eslint-disable-next-line no-console
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');
@@ -2,7 +2,10 @@
2
2
  // @ts-nocheck
3
3
  if (typeof window === 'undefined') {
4
4
  const getAny = () => {
5
- const anonymous = () => getAny();
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
- globalThis.HTMLElement = null;
25
- globalThis.location = new URL('about:blank');
26
- globalThis.window = 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 {};
@@ -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
- // https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow#options
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({ isAsync, isLight, delegatesFocus, slotAssignment }: GemElementOptions = {}) {
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 ? this : this.attachShadow({ mode: 'open', delegatesFocus, slotAssignment });
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` 前执行回调,和 `effect` 一样接受依赖数组参数,在 `constructor`/`willMount` 中使用;
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?.forEach(({ preCallback }) => execCallback(preCallback));
448
- this.#effectList = this.#effectList?.filter(this.#filterEffect);
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
- #filterEffect = (e: EffectItem<any>) => {
454
- e.initialized = false;
455
- return e.inConstructor;
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 = ([...ele.querySelectorAll('[data-gem-reflect]')] as GemReflectElement[]).map(
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;
@@ -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 & { title: string; path: string; query: QueryString; hash: string };
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 = paramsMap.get(store.$key) || ({} as HistoryParams);
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 query = new QueryString(params.query || (params.path ? '' : location.search));
69
- const pathChanged =
70
- (params.path && params.path !== current.path) || (params.query && String(params.query) !== String(current.query));
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 || (pathChanged ? '' : document.title);
84
+ const title = params.title || (urlChanged ? '' : document.title);
73
85
  const statusChanged = params.close || params.data || params.open || params.shouldClose;
74
- // 没提供 hash 又没有改变 URL 又不是状态更新时使用当前 hash
75
- const hash = params.hash || (!pathChanged && statusChanged ? decodeURIComponent(location.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: decodeURIComponent(location.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
  });