@mantou/gem 1.7.8 → 1.7.9

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 (63) hide show
  1. package/dist/gem.es.js +234 -102
  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/link.d.ts.map +1 -1
  7. package/elements/base/link.js +8 -4
  8. package/elements/base/link.js.map +1 -1
  9. package/elements/base/modal-factory.d.ts +28 -6
  10. package/elements/base/modal-factory.d.ts.map +1 -1
  11. package/elements/base/reflect.d.ts +8 -0
  12. package/elements/base/reflect.d.ts.map +1 -1
  13. package/elements/base/reflect.js +36 -9
  14. package/elements/base/reflect.js.map +1 -1
  15. package/elements/base/route.d.ts +25 -10
  16. package/elements/base/route.d.ts.map +1 -1
  17. package/elements/base/route.js +90 -56
  18. package/elements/base/route.js.map +1 -1
  19. package/elements/base/title.d.ts.map +1 -1
  20. package/elements/base/title.js +8 -2
  21. package/elements/base/title.js.map +1 -1
  22. package/helper/i18n.d.ts +8 -1
  23. package/helper/i18n.d.ts.map +1 -1
  24. package/helper/i18n.js +6 -2
  25. package/helper/i18n.js.map +1 -1
  26. package/helper/logger.d.ts +9 -0
  27. package/helper/logger.d.ts.map +1 -0
  28. package/helper/logger.js +16 -0
  29. package/helper/logger.js.map +1 -0
  30. package/helper/ssr-shim.d.ts.map +1 -1
  31. package/helper/ssr-shim.js +12 -4
  32. package/helper/ssr-shim.js.map +1 -1
  33. package/lib/element.d.ts +5 -5
  34. package/lib/element.d.ts.map +1 -1
  35. package/lib/element.js +40 -14
  36. package/lib/element.js.map +1 -1
  37. package/lib/history.d.ts +3 -2
  38. package/lib/history.d.ts.map +1 -1
  39. package/lib/history.js +22 -9
  40. package/lib/history.js.map +1 -1
  41. package/lib/utils.d.ts +2 -1
  42. package/lib/utils.d.ts.map +1 -1
  43. package/lib/utils.js +10 -8
  44. package/lib/utils.js.map +1 -1
  45. package/lib/version.d.ts +1 -1
  46. package/lib/version.js +1 -1
  47. package/package.json +1 -1
  48. package/src/elements/base/dialog.ts +4 -1
  49. package/src/elements/base/link.ts +8 -4
  50. package/src/elements/base/reflect.ts +43 -9
  51. package/src/elements/base/route.ts +102 -51
  52. package/src/elements/base/title.ts +9 -2
  53. package/src/helper/i18n.ts +11 -3
  54. package/src/helper/logger.ts +21 -0
  55. package/src/helper/ssr-shim.ts +14 -4
  56. package/src/lib/element.ts +46 -17
  57. package/src/lib/history.ts +30 -12
  58. package/src/lib/utils.ts +16 -9
  59. package/src/lib/version.ts +1 -1
  60. package/src/test/elements/link.test.ts +15 -0
  61. package/src/test/elements/reflect.test.ts +27 -0
  62. package/src/test/elements/title.test.ts +25 -0
  63. package/src/test/utils.test.ts +14 -0
@@ -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
  });
package/src/lib/utils.ts CHANGED
@@ -32,14 +32,24 @@ export function addMicrotaskToStack(func: () => void) {
32
32
  microtaskStack.push(func);
33
33
  }
34
34
 
35
+ // 不编码 hash 用于比较
35
36
  export function absoluteLocation(currentPath = '', relativePath = '') {
36
37
  const { pathname, search, hash } = new URL(relativePath, location.origin + currentPath);
37
- return pathname + search + hash;
38
+ return pathname + search + decodeURIComponent(hash);
38
39
  }
39
40
 
40
- // eslint-disable-next-line @typescript-eslint/ban-types
41
41
  export type NonPrimitive = object;
42
42
 
43
+ export function addListener<T extends EventTarget, A extends Parameters<T['addEventListener']>>(
44
+ target: T,
45
+ type: A[0],
46
+ listener: A[1],
47
+ options?: A[2],
48
+ ) {
49
+ target.addEventListener(type, listener, options);
50
+ return () => target.removeEventListener(type, listener, options);
51
+ }
52
+
43
53
  interface LinkedListEventMap {
44
54
  start: Event;
45
55
  end: Event;
@@ -49,7 +59,7 @@ export interface LinkedListItem<T> {
49
59
  prev?: LinkedListItem<T>;
50
60
  next?: LinkedListItem<T>;
51
61
  }
52
- // work on nodejs
62
+
53
63
  export class LinkedList<T = any> extends EventTarget {
54
64
  declare addEventListener: <K extends keyof LinkedListEventMap>(
55
65
  type: K,
@@ -269,12 +279,9 @@ export function createCSSSheet<T extends StyledKeyValuePair>(rules: T | string,
269
279
  return sheet as Sheet<T>;
270
280
  }
271
281
 
272
- export function randomStr(number = 5, origin = '0123456789abcdefghijklmnopqrstuvwxyz') {
273
- const len = origin.length;
274
- let str = '';
275
- for (let i = 0; i < number; i++) {
276
- str += origin[Math.floor(Math.random() * len)];
277
- }
282
+ export function randomStr(len = 5): string {
283
+ const str = Math.random().toString(36).slice(2).slice(0, len);
284
+ if (str.length < len) return str + randomStr(len - str.length);
278
285
  return str;
279
286
  }
280
287
 
@@ -1 +1 @@
1
- export const version = '1.7.8';
1
+ export const version = '1.7.9';
@@ -0,0 +1,15 @@
1
+ import { fixture, expect } from '@open-wc/testing';
2
+
3
+ import { html } from '../../lib/element';
4
+ import type { GemActiveLinkElement } from '../../elements/link';
5
+
6
+ import '../../elements/link';
7
+
8
+ describe('GemTitleElement', () => {
9
+ it('basic', async () => {
10
+ const ele: GemActiveLinkElement = await fixture(
11
+ html`<gem-active-link pattern="/*" href="/#测试">active-link</gem-active-link>`,
12
+ );
13
+ expect(ele.active).to.equal(true);
14
+ });
15
+ });
@@ -0,0 +1,27 @@
1
+ import { fixture, expect, nextFrame } from '@open-wc/testing';
2
+
3
+ import { html } from '../../lib/element';
4
+ import type { GemReflectElement } from '../../elements/reflect';
5
+
6
+ import '../../elements/reflect';
7
+
8
+ describe('GemReflectElement', () => {
9
+ it('basic', async () => {
10
+ const reflect: GemReflectElement = await fixture(html`
11
+ <gem-reflect>
12
+ <div id="one"></div>
13
+ <div id="two"></div>
14
+ </gem-reflect>
15
+ `);
16
+ const div = document.head.querySelector('#one');
17
+ expect(!!div).to.equal(true);
18
+ const comment = new Comment();
19
+ div?.after(comment);
20
+ reflect.target = document.body;
21
+ await nextFrame();
22
+ expect(document.body.querySelector('#one')).to.equal(div);
23
+ expect(div?.nextSibling).to.equal(comment);
24
+ reflect.remove();
25
+ expect(!!document.querySelector('#one')).to.equal(false);
26
+ });
27
+ });
@@ -0,0 +1,25 @@
1
+ import { fixture, expect, nextFrame } from '@open-wc/testing';
2
+
3
+ import { html } from '../../lib/element';
4
+ import type { GemTitleElement } from '../../elements/title';
5
+
6
+ import '../../elements/title';
7
+
8
+ describe('GemTitleElement', () => {
9
+ it('basic', async () => {
10
+ const ele: GemTitleElement = await fixture(html`<gem-title>title</gem-title>`);
11
+ expect(document.title).to.equal('title');
12
+ ele.textContent = 'title2';
13
+ await nextFrame();
14
+ expect(document.title).to.equal('title2');
15
+
16
+ ele.prefix = 'prefix-';
17
+ await nextFrame();
18
+ expect(document.title).to.equal('prefix-title2');
19
+
20
+ history.pushState(null, 'Page', '/');
21
+ await nextFrame();
22
+ expect(document.title).to.equal('prefix-Page');
23
+ expect(ele.shadowRoot!.textContent).to.equal('Page');
24
+ });
25
+ });
@@ -25,6 +25,8 @@ describe('utils 测试', () => {
25
25
  expect(absoluteLocation('/a', '/a/b')).to.equal('/a/b');
26
26
  expect(absoluteLocation('/a/c', './b')).to.equal('/a/b');
27
27
  expect(absoluteLocation('/a/c/d', '../b')).to.equal('/a/b');
28
+ expect(absoluteLocation('/a/c/d', '../b?a=a#a')).to.equal('/a/b?a=a#a');
29
+ expect(absoluteLocation('/a/c/d', '../b?a=测试#测试#')).to.equal('/a/b?a=%E6%B5%8B%E8%AF%95#测试#');
28
30
  });
29
31
  it('LinkedList', () => {
30
32
  const linkedList = new LinkedList<() => void>();
@@ -36,6 +38,18 @@ describe('utils 测试', () => {
36
38
  const fun2 = () => 2;
37
39
  linkedList.add(fun1);
38
40
  linkedList.add(fun2);
41
+ expect(linkedList.first?.value).to.equal(fun1);
42
+ expect(linkedList.last?.value).to.equal(fun2);
43
+ expect(linkedList.find(fun1)).to.equal(linkedList.first);
44
+ expect(linkedList.find(fun2)).to.equal(linkedList.last);
45
+ expect(linkedList.find(fun1)?.next).to.equal(linkedList.last);
46
+ const subLinked = new LinkedList();
47
+ subLinked.add(fun1);
48
+ subLinked.add(fun2);
49
+ expect(linkedList.isSuperLinkOf(subLinked)).to.equal(true);
50
+ expect(new LinkedList().isSuperLinkOf(subLinked)).to.equal(false);
51
+ expect(subLinked.isSuperLinkOf(new LinkedList())).to.equal(true);
52
+
39
53
  linkedList.delete(fun2);
40
54
  expect(linkedList.size).to.equal(1);
41
55
  linkedList.add(fun2);