@mantou/gem 1.5.3 → 1.5.7

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 (49) hide show
  1. package/elements/dialog-base.js +1 -1
  2. package/elements/dialog-base.js.map +1 -1
  3. package/elements/modal-base.d.ts +45 -34
  4. package/elements/modal-base.d.ts.map +1 -1
  5. package/elements/modal-base.js +1 -1
  6. package/elements/modal-base.js.map +1 -1
  7. package/elements/route.d.ts +2 -2
  8. package/elements/route.d.ts.map +1 -1
  9. package/elements/route.js +11 -7
  10. package/elements/route.js.map +1 -1
  11. package/elements/use.d.ts +1 -0
  12. package/elements/use.d.ts.map +1 -1
  13. package/elements/use.js +4 -1
  14. package/elements/use.js.map +1 -1
  15. package/helper/i18n.d.ts +2 -4
  16. package/helper/i18n.d.ts.map +1 -1
  17. package/helper/i18n.js +69 -34
  18. package/helper/i18n.js.map +1 -1
  19. package/helper/mediaquery.d.ts +2 -0
  20. package/helper/mediaquery.d.ts.map +1 -1
  21. package/helper/mediaquery.js +18 -13
  22. package/helper/mediaquery.js.map +1 -1
  23. package/lib/element.d.ts +5 -5
  24. package/lib/element.d.ts.map +1 -1
  25. package/lib/element.js +21 -23
  26. package/lib/element.js.map +1 -1
  27. package/lib/utils.d.ts +4 -4
  28. package/lib/utils.d.ts.map +1 -1
  29. package/lib/utils.js +59 -31
  30. package/lib/utils.js.map +1 -1
  31. package/lib/version.d.ts +1 -1
  32. package/lib/version.js +1 -1
  33. package/package.json +7 -7
  34. package/src/elements/dialog-base.ts +1 -1
  35. package/src/elements/modal-base.ts +1 -1
  36. package/src/elements/route.ts +12 -7
  37. package/src/elements/use.ts +3 -1
  38. package/src/examples/i18n/index.ts +14 -0
  39. package/src/examples/multi-page/page-c.ts +1 -0
  40. package/src/examples/ref-route/index.ts +3 -2
  41. package/src/helper/i18n.ts +41 -13
  42. package/src/helper/mediaquery.ts +20 -13
  43. package/src/lib/element.ts +26 -27
  44. package/src/lib/utils.ts +47 -30
  45. package/src/lib/version.ts +1 -1
  46. package/src/test/gem-element/advance.test.ts +8 -2
  47. package/src/test/utils.test.ts +9 -6
  48. package/umd.js +6 -6
  49. package/umd.js.map +1 -1
@@ -60,6 +60,7 @@ export interface RouteItem<T = unknown> {
60
60
  // 支持 *
61
61
  // 支持 /a/*
62
62
  // 不支持相对路径
63
+ // 不能匹配查询字符串
63
64
  pattern: string;
64
65
  redirect?: string;
65
66
  content?: TemplateResult;
@@ -109,7 +110,7 @@ type State = {
109
110
  /**
110
111
  * @customElement gem-route
111
112
  * @attr inert 暂停路由更新
112
- * @fires change
113
+ * @fires route-change
113
114
  * @fires error
114
115
  * @fires loading
115
116
  */
@@ -124,7 +125,7 @@ export class GemRouteElement extends GemElement<State> {
124
125
  */
125
126
  @property locationStore?: Store<{ path: string; params: Params; query: QueryString; data?: any }>;
126
127
  @property key: any; // 除了 href 提供另外一种方式来更新,比如语言更新也需要刷新 <gem-route>
127
- @emitter change: Emitter<RouteItem | null>; // path 改变或者 key 改变,包含初始渲染
128
+ @emitter routeChange: Emitter<RouteItem | null>; // path 改变或者 key 改变,包含初始渲染
128
129
  @emitter loading: Emitter<RouteItem>;
129
130
  @emitter error: Emitter<any>;
130
131
 
@@ -186,7 +187,7 @@ export class GemRouteElement extends GemElement<State> {
186
187
  this.currentRoute = route;
187
188
  this.currentParams = params;
188
189
  this.setState({ content });
189
- this.change(this.currentRoute);
190
+ this.routeChange(this.currentRoute);
190
191
  const title = route?.title;
191
192
  if (title) updateStore(titleStore, { title });
192
193
  this.#updateLocationStore();
@@ -194,7 +195,12 @@ export class GemRouteElement extends GemElement<State> {
194
195
 
195
196
  mounted() {
196
197
  this.effect(
197
- ([_, path]) => {
198
+ ([key, path], old) => {
199
+ // 只有查询参数改变
200
+ if (old && key === old[0] && path === old[1]) {
201
+ this.#updateLocationStore();
202
+ return;
203
+ }
198
204
  const { route, params = {} } = GemRouteElement.findRoute(this.routes, path);
199
205
  const { redirect, content, getContent } = route || {};
200
206
  if (redirect) {
@@ -219,10 +225,9 @@ export class GemRouteElement extends GemElement<State> {
219
225
  },
220
226
  () => {
221
227
  const { path } = history.getParams();
222
- return [this.key, path];
228
+ return [this.key, path, location.search];
223
229
  },
224
230
  );
225
- this.effect(this.#updateLocationStore, () => [location.search]);
226
231
  }
227
232
 
228
233
  shouldUpdate() {
@@ -241,7 +246,7 @@ export class GemRouteElement extends GemElement<State> {
241
246
  display: contents;
242
247
  }
243
248
  </style>
244
- ${content}
249
+ ${content ?? html`<slot></slot>`}
245
250
  `;
246
251
  }
247
252
  }
@@ -1,4 +1,4 @@
1
- import { GemElement, html, attribute, property, customElement } from '../';
1
+ import { GemElement, html, attribute, property, customElement, state } from '../';
2
2
 
3
3
  const eleCache = new Map<string, HTMLTemplateElement>();
4
4
  /**
@@ -17,6 +17,8 @@ export class GemUseElement extends GemElement {
17
17
  @property root?: HTMLElement | Document | ShadowRoot;
18
18
  @property element?: string | DocumentFragment | Element;
19
19
 
20
+ @state active: boolean;
21
+
20
22
  private getContent() {
21
23
  const ele = this.element || (this.selector ? (this.root || document).querySelector(this.selector) : null);
22
24
  if (typeof ele === 'string') {
@@ -22,7 +22,16 @@ const i18n = new I18n<typeof en>({
22
22
  onChange: console.log,
23
23
  });
24
24
 
25
+ const i18nModule = i18n.createSubModule<typeof en>('test', {
26
+ en,
27
+ de: {
28
+ title: 'Das ist I18n',
29
+ },
30
+ zh: 'data:text/plain;base64,eyJ0aXRsZSI6Iui/meaYr0kxOG4ifQ==',
31
+ });
32
+
25
33
  @connectStore(i18n.store)
34
+ @connectStore(i18nModule.store)
26
35
  @customElement('app-root')
27
36
  export class App extends GemElement {
28
37
  render() {
@@ -35,6 +44,11 @@ export class App extends GemElement {
35
44
  <p>${i18n.get('title')}</p>
36
45
  <p>${i18n.get('hello', 'World', 'reverse')}</p>
37
46
  <p>${i18n.get('detail', (s) => html`<a href="#">${s}</a>`)}</p>
47
+
48
+ <h2>sub module</h2>
49
+ <p>${i18nModule.get('title')}</p>
50
+ <p>${i18nModule.get('hello', 'World', 'reverse')}</p>
51
+ <p>${i18nModule.get('detail', (s) => html`<a href="#">${s}</a>`)}</p>
38
52
  `;
39
53
  }
40
54
  }
@@ -1,5 +1,6 @@
1
1
  import { GemElement, html } from '../../';
2
2
  import '../../elements/route';
3
+ import '../../elements/link';
3
4
 
4
5
  const routes = [
5
6
  {
@@ -7,7 +7,8 @@ import '../elements/layout';
7
7
 
8
8
  const homeRoute: RouteItem = {
9
9
  pattern: '/',
10
- getContent(params) {
10
+ async getContent(params) {
11
+ await new Promise((res) => setTimeout(res, 1000));
11
12
  return html`<route-home .params=${params}></route-home>`;
12
13
  },
13
14
  };
@@ -88,7 +89,7 @@ export class App extends GemElement {
88
89
  <gem-route
89
90
  ref=${this.routeRef.ref}
90
91
  @loading=${console.log}
91
- @change=${console.log}
92
+ @route-change=${console.log}
92
93
  @error=${console.error}
93
94
  .routes=${routes}
94
95
  .locationStore=${locationStore}
@@ -8,6 +8,7 @@ import { GemError } from '../lib/utils';
8
8
  const splitReg = /\$\d(?:<[^>]*>)?/;
9
9
  const matchReg = /\$(\d)(<([^>]*)>)?/g;
10
10
  const matchStrIndex = 3;
11
+ const modules = new Set<I18n<any>>();
11
12
 
12
13
  type Msg =
13
14
  | {
@@ -56,11 +57,11 @@ export class I18n<T = Record<string, Msg>> {
56
57
 
57
58
  store: Store<any>;
58
59
 
59
- get cacheCurrentKey() {
60
+ get #cacheCurrentKey() {
60
61
  return `${this.cachePrefix}:current`;
61
62
  }
62
63
 
63
- get urlParamsLang() {
64
+ get #urlParamsLang() {
64
65
  const { hostname, pathname, search } = location;
65
66
  const parts = hostname.split('.');
66
67
  switch (this.urlParamsType) {
@@ -76,13 +77,19 @@ export class I18n<T = Record<string, Msg>> {
76
77
  }
77
78
  }
78
79
 
79
- get cacheLang() {
80
- return (this.cache && localStorage.getItem(this.cacheCurrentKey)) || '';
80
+ get #cacheLang() {
81
+ return (this.cache && localStorage.getItem(this.#cacheCurrentKey)) || '';
81
82
  }
82
83
 
83
- set lang(lang: string) {
84
+ get #isMain(): boolean {
85
+ return !modules.has(this);
86
+ }
87
+
88
+ set #lang(lang: string) {
89
+ if (this.#isMain) {
90
+ document.documentElement.lang = lang;
91
+ }
84
92
  this.currentLanguage = lang;
85
- document.documentElement.lang = lang;
86
93
  this.onChange?.(lang);
87
94
  }
88
95
 
@@ -94,7 +101,7 @@ export class I18n<T = Record<string, Msg>> {
94
101
  this.store = createStore(this as any);
95
102
  Object.assign<I18n<T>, Options<T>>(this as I18n<T>, options);
96
103
 
97
- let currentLanguage = this.urlParamsLang || this.currentLanguage || this.cacheLang;
104
+ let currentLanguage = this.#urlParamsLang || this.currentLanguage || this.#cacheLang;
98
105
 
99
106
  const ele = document.querySelector(`[type*=${this.cachePrefix}]`);
100
107
  if (ele) {
@@ -108,7 +115,7 @@ export class I18n<T = Record<string, Msg>> {
108
115
  }
109
116
 
110
117
  if (currentLanguage) {
111
- this.lang = currentLanguage;
118
+ this.#lang = currentLanguage;
112
119
  this.setLanguage(currentLanguage);
113
120
  } else {
114
121
  this.resetLanguage();
@@ -147,11 +154,14 @@ export class I18n<T = Record<string, Msg>> {
147
154
  }
148
155
 
149
156
  async setLanguage(lang: string): Promise<string> {
157
+ if (this.#isMain) {
158
+ modules.forEach(async (i18n) => await i18n.setLanguage(lang));
159
+ }
150
160
  let pack: Partial<T>;
151
161
  const data = this.resources[lang];
152
162
  if (!data) {
153
163
  // eslint-disable-next-line no-console
154
- console.warn(`i18n: not found \`${lang}\``);
164
+ console.warn(`${this.cachePrefix}: not found \`${lang}\``);
155
165
  return this.resetLanguage();
156
166
  }
157
167
 
@@ -190,7 +200,7 @@ export class I18n<T = Record<string, Msg>> {
190
200
  this.resources[lang] = pack;
191
201
  updateStore(this.store, {});
192
202
  if (lang !== this.currentLanguage) {
193
- this.lang = lang;
203
+ this.#lang = lang;
194
204
  }
195
205
  // auto cache
196
206
  if (this.cache === true) {
@@ -200,16 +210,34 @@ export class I18n<T = Record<string, Msg>> {
200
210
  }
201
211
 
202
212
  resetLanguage() {
203
- this.lang = this.fallbackLanguage;
213
+ if (this.#isMain) {
214
+ modules.forEach((i18n) => i18n.resetLanguage());
215
+ }
216
+ this.#lang = this.fallbackLanguage;
204
217
  if (this.cache) {
205
- localStorage.removeItem(this.cacheCurrentKey);
218
+ localStorage.removeItem(this.#cacheCurrentKey);
206
219
  }
207
220
  return this.setLanguage(this.detectLanguage());
208
221
  }
209
222
 
210
223
  // 一般用在 `onChange` 中
211
224
  setCache() {
212
- localStorage.setItem(this.cacheCurrentKey, this.currentLanguage);
225
+ if (this.#isMain) {
226
+ modules.forEach((i18n) => i18n.setCache());
227
+ }
228
+ localStorage.setItem(this.#cacheCurrentKey, this.currentLanguage);
229
+ }
230
+
231
+ createSubModule<K = Record<string, Msg>>(name: string, resources: Resources<K>) {
232
+ const module = new I18n<K>({
233
+ resources,
234
+ cachePrefix: `${this.cachePrefix}-${name}`,
235
+ currentLanguage: this.currentLanguage,
236
+ fallbackLanguage: this.fallbackLanguage,
237
+ cache: this.cache,
238
+ });
239
+ modules.add(module);
240
+ return module;
213
241
  }
214
242
 
215
243
  /**
@@ -1,70 +1,77 @@
1
1
  // https://gist.github.com/gokulkrishh/242e68d1ee94ad05f488
2
2
  // https://mediag.com/news/popular-screen-resolutions-designing-for-all/
3
3
 
4
+ const matche = (query: string) => window.matchMedia(query).matches;
5
+
4
6
  export const mediaQuery = {
5
7
  PRINT: 'print',
6
8
  get isPrint() {
7
- return window.matchMedia(this.PRINT).matches;
9
+ return matche(this.PRINT);
8
10
  },
9
11
  HOVER: '(hover: hover)',
10
12
  get isHover() {
11
- return window.matchMedia(this.HOVER).matches;
13
+ return matche(this.HOVER);
12
14
  },
13
15
 
14
16
  PHONE: '(min-width: 319px) and (max-width: 480px)',
15
17
  get isPhone() {
16
- return window.matchMedia(this.PHONE).matches;
18
+ return matche(this.PHONE);
17
19
  },
18
20
 
19
21
  LAPTOP: '(min-width: 1023px) and (max-width: 1280px) and (orientation: landscape)',
20
22
  get isLaptop() {
21
- return window.matchMedia(this.LAPTOP).matches;
23
+ return matche(this.LAPTOP);
22
24
  },
23
25
 
24
26
  DESKTOP: '(min-width: 1279px) and (orientation: landscape)',
25
27
  get isDesktop() {
26
- return window.matchMedia(this.DESKTOP).matches;
28
+ return matche(this.DESKTOP);
29
+ },
30
+
31
+ WIDTHSCREEN: '(min-width: 1919px) and (orientation: landscape)',
32
+ get isWidthScreen() {
33
+ return matche(this.WIDTHSCREEN);
27
34
  },
28
35
 
29
36
  PHONE_LANDSCAPE: '(min-width: 479px) and (max-width: 767px) and (orientation: landscape)',
30
37
  // PHONE_LANDSCAPE: '(min-width: 481px) and (max-width: 767px)',
31
38
  get isPhoneLandscape() {
32
- return window.matchMedia(this.PHONE_LANDSCAPE).matches;
39
+ return matche(this.PHONE_LANDSCAPE);
33
40
  },
34
41
 
35
42
  TABLET: '(min-width: 768px) and (max-width: 1024px)',
36
43
  get isTablet() {
37
- return window.matchMedia(this.TABLET).matches;
44
+ return matche(this.TABLET);
38
45
  },
39
46
 
40
47
  SMALL_PHONE: '(min-width: 319px) and (max-width: 480px) and (max-height: 640px)',
41
48
  get isSmallPhone() {
42
- return window.matchMedia(this.SMALL_PHONE).matches;
49
+ return matche(this.SMALL_PHONE);
43
50
  },
44
51
 
45
52
  WATCH: '(max-width: 319px)',
46
53
  get isWatch() {
47
- return window.matchMedia(this.WATCH).matches;
54
+ return matche(this.WATCH);
48
55
  },
49
56
 
50
57
  SHORT: '(min-width: 479px) and (max-height: 320px)',
51
58
  get isShort() {
52
- return window.matchMedia(this.SHORT).matches;
59
+ return matche(this.SHORT);
53
60
  },
54
61
 
55
62
  PWA: '(display-mode: standalone)',
56
63
  // PWA: '(display-mode: browser)', // debugging
57
64
  get isPWA() {
58
- return window.matchMedia(this.PWA).matches;
65
+ return matche(this.PWA);
59
66
  },
60
67
 
61
68
  MOTION_REDUCE: '(prefers-reduced-motion: reduce)',
62
69
  get isMotionReduce() {
63
- return window.matchMedia(this.MOTION_REDUCE).matches;
70
+ return matche(this.MOTION_REDUCE);
64
71
  },
65
72
 
66
73
  DATA_REDUCE: '(prefers-reduced-data: reduce)',
67
74
  get isDataReduce() {
68
- return window.matchMedia(this.DATA_REDUCE).matches;
75
+ return matche(this.DATA_REDUCE);
69
76
  },
70
77
  };
@@ -25,11 +25,10 @@ export { ifDefined } from 'lit-html/directives/if-defined';
25
25
  type CustomStateSet = Set<string>;
26
26
 
27
27
  declare global {
28
- interface ElementInternals {
28
+ interface ElementInternals extends ARIAMixin {
29
29
  states: CustomStateSet;
30
30
  // https://w3c.github.io/aria/#role_definitions
31
31
  role?: string;
32
- ariaModal?: boolean;
33
32
  }
34
33
  // 用于 css 选择器选择元素,使用 @refobject 自动选择获取
35
34
  // 必须使用 attr 赋值
@@ -38,8 +37,8 @@ declare global {
38
37
  * @attr inert
39
38
  */
40
39
  interface HTMLElement {
41
- attachInternals?: () => ElementInternals;
42
- inert?: boolean;
40
+ attachInternals: () => ElementInternals;
41
+ inert: boolean;
43
42
  }
44
43
  }
45
44
 
@@ -53,20 +52,15 @@ function execCallback(fun: any) {
53
52
 
54
53
  // global render task pool
55
54
  const asyncRenderTaskList = new LinkedList<() => void>();
56
- const tick = () => {
57
- window.requestAnimationFrame(function callback(timestamp) {
58
- if (performance.now() > timestamp + 16) {
59
- tick();
60
- return;
61
- }
62
- const task = asyncRenderTaskList.get();
63
- if (task) {
64
- task();
65
- callback(timestamp);
66
- }
67
- });
55
+ const tick = (timeStamp = performance.now()) => {
56
+ if (performance.now() > timeStamp + 16) return requestAnimationFrame(tick);
57
+ const task = asyncRenderTaskList.get();
58
+ if (task) {
59
+ task();
60
+ tick(timeStamp);
61
+ }
68
62
  };
69
- asyncRenderTaskList.addEventListener('start', tick);
63
+ asyncRenderTaskList.addEventListener('start', () => addMicrotask(tick));
70
64
 
71
65
  type GetDepFun<T> = () => T;
72
66
  type EffectCallback<T> = (depValues: T, oldDepValues?: T) => any;
@@ -164,7 +158,7 @@ export abstract class GemElement<T = Record<string, unknown>> extends HTMLElemen
164
158
  if (!this.attachInternals) {
165
159
  // https://bugs.webkit.org/show_bug.cgi?id=197960
166
160
  this.attachInternals = () => {
167
- return { states: getCustomStateSet() };
161
+ return { states: getCustomStateSet() } as any;
168
162
  };
169
163
  }
170
164
  this.#internals = this.attachInternals();
@@ -198,7 +192,7 @@ export abstract class GemElement<T = Record<string, unknown>> extends HTMLElemen
198
192
  list?.forEach((effectItem) => {
199
193
  const { callback, getDep, values, preCallback } = effectItem;
200
194
  const newValues = getDep?.();
201
- if (!getDep || isArrayChange(values, newValues)) {
195
+ if (!getDep || !values || isArrayChange(values, newValues)) {
202
196
  execCallback(preCallback);
203
197
  effectItem.preCallback = callback(newValues, values);
204
198
  effectItem.values = newValues;
@@ -235,15 +229,17 @@ export abstract class GemElement<T = Record<string, unknown>> extends HTMLElemen
235
229
  getDep?: T extends any[] ? () => [...T] : undefined,
236
230
  ) => {
237
231
  if (!this.#effectList) this.#effectList = [];
238
- const values = getDep?.() as T;
239
232
  const effectItem: EffectItem<T> = {
240
233
  callback,
241
234
  getDep,
242
- values,
235
+ values: undefined,
243
236
  initialized: !!this.isConnected,
244
237
  };
245
238
  // 以挂载时立即执行副作用,未挂载时等挂载后执行
246
- if (this.isConnected) effectItem.preCallback = callback(values);
239
+ if (this.isConnected) {
240
+ effectItem.values = getDep?.() as T;
241
+ effectItem.preCallback = callback(effectItem.values);
242
+ }
247
243
  this.#effectList.push(effectItem);
248
244
  };
249
245
 
@@ -279,7 +275,8 @@ export abstract class GemElement<T = Record<string, unknown>> extends HTMLElemen
279
275
  this.#effectList?.forEach((effectItem) => {
280
276
  const { callback, getDep, initialized } = effectItem;
281
277
  if (!initialized) {
282
- effectItem.preCallback = callback(getDep?.());
278
+ effectItem.values = getDep?.();
279
+ effectItem.preCallback = callback(effectItem.values);
283
280
  effectItem.initialized = true;
284
281
  }
285
282
  });
@@ -378,14 +375,16 @@ export abstract class GemElement<T = Record<string, unknown>> extends HTMLElemen
378
375
  }
379
376
  };
380
377
 
381
- closestElement(clsOrName: CustomElementConstructor | string) {
382
- const isConstructor = typeof clsOrName === 'function';
383
- const tagName = typeof clsOrName === 'string' && clsOrName.toUpperCase();
378
+ closestElement<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElementTagNameMap[K] | null;
379
+ closestElement<K extends abstract new (...args: any) => any>(constructor: K): InstanceType<K> | null;
380
+ closestElement<K extends abstract new (...args: any) => any>(constructorOrTag: K | string): Element | null {
381
+ const isConstructor = typeof constructorOrTag === 'function';
382
+ const tagName = typeof constructorOrTag === 'string' && constructorOrTag.toUpperCase();
384
383
  const getRootElement = (ele: Element): Element | null => {
385
384
  const rootEle = ele.parentElement || (ele.getRootNode() as ShadowRoot).host;
386
385
  if (!rootEle) return null;
387
386
  if (isConstructor) {
388
- if (rootEle.constructor === clsOrName) {
387
+ if (rootEle.constructor === constructorOrTag) {
389
388
  return rootEle;
390
389
  }
391
390
  } else if (rootEle.tagName === tagName) {
package/src/lib/utils.ts CHANGED
@@ -50,56 +50,72 @@ interface LinkedListItem<T> {
50
50
  next?: LinkedListItem<T>;
51
51
  }
52
52
  // work on nodejs
53
- export class LinkedList<T extends NonPrimitive> extends (globalThis.EventTarget || Object) {
53
+ export class LinkedList<T = any> extends (globalThis.EventTarget || Object) {
54
54
  addEventListener: <K extends keyof LinkedListEventMap>(
55
55
  type: K,
56
56
  listener: (this: LinkedList<T>, ev: LinkedListEventMap[K]) => any,
57
57
  options?: boolean | AddEventListenerOptions,
58
58
  ) => void;
59
59
 
60
- pool = new Map<T, LinkedListItem<T>>();
61
- firstItem?: LinkedListItem<T>;
62
- lastItem?: LinkedListItem<T>;
60
+ #map = new Map<T, LinkedListItem<T>>();
61
+ #firstItem?: LinkedListItem<T>;
62
+ #lastItem?: LinkedListItem<T>;
63
63
 
64
- // 添加已存在的项目时会删除老的项目
65
- add(value: T) {
66
- const item: LinkedListItem<T> = { value };
67
- if (!this.firstItem) {
68
- this.dispatchEvent(new CustomEvent('start'));
69
- this.firstItem = item;
70
- }
71
- if (this.lastItem) {
72
- this.lastItem.next = item;
73
- item.prev = this.lastItem;
74
- }
75
- this.lastItem = item;
76
-
77
- const existItem = this.pool.get(value);
64
+ #delete(value: T) {
65
+ const existItem = this.#map.get(value);
78
66
  if (existItem) {
79
67
  if (existItem.prev) {
80
68
  existItem.prev.next = existItem.next;
81
69
  } else {
82
- this.firstItem = existItem.next;
70
+ this.#firstItem = existItem.next;
83
71
  }
84
72
  if (existItem.next) {
85
73
  existItem.next.prev = existItem.prev;
74
+ } else {
75
+ this.#lastItem = existItem.prev;
86
76
  }
77
+ this.#map.delete(value);
87
78
  }
88
-
89
- this.pool.set(value, item);
79
+ return existItem;
90
80
  }
91
81
 
92
- get(): T | undefined {
93
- const firstItem = this.firstItem;
94
- if (!firstItem) return;
82
+ get size() {
83
+ return this.#map.size;
84
+ }
95
85
 
96
- this.pool.delete(firstItem.value);
86
+ // 添加到尾部,已存在时会删除老的项目
87
+ // 如果是添加第一个,start 事件会在添加前触发,避免处理事件重复的逻辑
88
+ add(value: T) {
89
+ if (!this.#lastItem) {
90
+ this.dispatchEvent(new CustomEvent('start'));
91
+ }
92
+ const item: LinkedListItem<T> = this.#delete(value) || { value };
93
+ item.prev = this.#lastItem;
94
+ if (item.prev) {
95
+ item.prev.next = item;
96
+ }
97
+ item.next = undefined;
98
+ this.#lastItem = item;
99
+ if (!this.#firstItem) {
100
+ this.#firstItem = item;
101
+ }
102
+ this.#map.set(value, item);
103
+ }
97
104
 
98
- this.firstItem = firstItem.next;
99
- if (!this.firstItem) {
100
- this.lastItem = undefined;
105
+ // 删除这个元素后没有其他元素时立即出发 end 事件
106
+ delete(value: T) {
107
+ const deleteItem = this.#delete(value);
108
+ if (!this.#firstItem) {
101
109
  this.dispatchEvent(new CustomEvent('end'));
102
110
  }
111
+ return deleteItem;
112
+ }
113
+
114
+ // 获取头部元素
115
+ get(): T | undefined {
116
+ const firstItem = this.#firstItem;
117
+ if (!firstItem) return;
118
+ this.#delete(firstItem.value);
103
119
  return firstItem.value;
104
120
  }
105
121
  }
@@ -125,6 +141,7 @@ export class QueryString extends URLSearchParams {
125
141
  super(param);
126
142
  }
127
143
 
144
+ // support `{ key: ObjectValue }`
128
145
  concat(param: any) {
129
146
  let query: any;
130
147
  if (typeof param === 'string') {
@@ -132,8 +149,8 @@ export class QueryString extends URLSearchParams {
132
149
  } else {
133
150
  query = param;
134
151
  }
135
- Object.keys(query).forEach((key) => {
136
- this.append(key, query[key]);
152
+ Object.entries(query).forEach(([key, value]) => {
153
+ this.append(key, this.#stringify(value));
137
154
  });
138
155
  }
139
156
 
@@ -1,2 +1,2 @@
1
1
  // Do not modify manually
2
- export const version = '1.5.3';
2
+ export const version = '1.5.7';
@@ -25,10 +25,11 @@ customElements.define('async-gem-demo', AsyncGemDemo);
25
25
  describe('异步 gem element 测试', () => {
26
26
  it('异步 gem element 更新', async () => {
27
27
  const el: AsyncGemDemo = await fixture(html`<async-gem-demo></async-gem-demo>`);
28
+ expect(el.renderCount).to.equal(1);
28
29
  updateStore(store, { a: ++store.a });
29
30
  el.setState({ a: ++el.state.a });
30
31
  await nextFrame();
31
- expect(el.renderCount).to.equal(1);
32
+ expect(el.renderCount).to.equal(2);
32
33
  await nextFrame();
33
34
  expect(el.renderCount).to.equal(2);
34
35
  });
@@ -146,11 +147,15 @@ class EffectGemDemo extends GemElement {
146
147
  @attribute attr = 'a';
147
148
  @property prop = {};
148
149
  effectCount = 0;
150
+ compareCount = 0;
149
151
  constructor() {
150
152
  super();
151
153
  this.effect(
152
154
  () => this.effectCount++,
153
- () => [this.attr, this.prop],
155
+ () => {
156
+ this.compareCount++;
157
+ return [this.attr, this.prop];
158
+ },
154
159
  );
155
160
  }
156
161
  mounted() {
@@ -170,6 +175,7 @@ customElements.define('effect-gem-demo', EffectGemDemo);
170
175
  describe('gem element 副作用', () => {
171
176
  it('依赖当前值', async () => {
172
177
  const el: EffectGemDemo = await fixture(html`<effect-gem-demo></effect-gem-demo>`);
178
+ expect(el.compareCount).to.equal(1);
173
179
  expect(el.effectCount).to.equal(3);
174
180
  el.attr = 'b';
175
181
  el.prop = {};
@@ -22,17 +22,20 @@ describe('utils 测试', () => {
22
22
  const linkedList = new LinkedList<() => void>();
23
23
  let countAtStart = 0;
24
24
  let countAtPause = 0;
25
- linkedList.addEventListener('start', () => (countAtStart = linkedList.pool.size));
26
- linkedList.addEventListener('end', () => (countAtPause = linkedList.pool.size));
27
- const fun1 = () => ({});
28
- const fun2 = () => ({});
25
+ linkedList.addEventListener('start', () => (countAtStart = linkedList.size));
26
+ linkedList.addEventListener('end', () => (countAtPause = linkedList.size));
27
+ const fun1 = () => 1;
28
+ const fun2 = () => 2;
29
29
  linkedList.add(fun1);
30
30
  linkedList.add(fun2);
31
+ linkedList.delete(fun2);
32
+ expect(linkedList.size).to.equal(1);
33
+ linkedList.add(fun2);
31
34
  linkedList.add(fun1);
32
- expect(linkedList.pool.size).to.equal(2);
35
+ expect(linkedList.size).to.equal(2);
33
36
  expect(linkedList.get()).to.equal(fun2);
34
37
  expect(linkedList.get()).to.equal(fun1);
35
- expect(linkedList.pool.size).to.equal(0);
38
+ expect(linkedList.size).to.equal(0);
36
39
  expect(linkedList.get()).to.equal(undefined);
37
40
  expect(countAtStart).to.equal(0);
38
41
  expect(countAtPause).to.equal(0);