@mantou/gem 2.1.1 → 2.2.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.
- package/dist/gem.es.js +2082 -2153
- package/dist/gem.umd.js +25 -199
- package/elements/base/dialog.d.ts +1 -5
- package/elements/base/dialog.d.ts.map +1 -1
- package/elements/base/dialog.js +2 -6
- package/elements/base/dialog.js.map +1 -1
- package/elements/base/gesture.d.ts +6 -15
- package/elements/base/gesture.d.ts.map +1 -1
- package/elements/base/gesture.js +38 -34
- package/elements/base/gesture.js.map +1 -1
- package/elements/base/link.d.ts +1 -17
- package/elements/base/link.d.ts.map +1 -1
- package/elements/base/link.js +12 -37
- package/elements/base/link.js.map +1 -1
- package/elements/base/modal-factory.d.ts +48 -38
- package/elements/base/modal-factory.d.ts.map +1 -1
- package/elements/base/modal-factory.js +14 -14
- package/elements/base/modal-factory.js.map +1 -1
- package/elements/base/reflect.d.ts +1 -5
- package/elements/base/reflect.d.ts.map +1 -1
- package/elements/base/reflect.js +8 -13
- package/elements/base/reflect.js.map +1 -1
- package/elements/base/route.d.ts +17 -17
- package/elements/base/route.d.ts.map +1 -1
- package/elements/base/route.js +15 -27
- package/elements/base/route.js.map +1 -1
- package/elements/base/title.d.ts +1 -6
- package/elements/base/title.d.ts.map +1 -1
- package/elements/base/title.js +3 -8
- package/elements/base/title.js.map +1 -1
- package/elements/base/unsafe.d.ts +1 -6
- package/elements/base/unsafe.d.ts.map +1 -1
- package/elements/base/unsafe.js +3 -9
- package/elements/base/unsafe.js.map +1 -1
- package/elements/base/use.d.ts +1 -4
- package/elements/base/use.d.ts.map +1 -1
- package/elements/base/use.js +4 -11
- package/elements/base/use.js.map +1 -1
- package/elements/link.js +1 -1
- package/elements/link.js.map +1 -1
- package/elements/route.js +1 -1
- package/elements/route.js.map +1 -1
- package/helper/hmr.d.ts +11 -0
- package/helper/hmr.d.ts.map +1 -0
- package/helper/hmr.js +255 -0
- package/helper/hmr.js.map +1 -0
- package/helper/i18n.d.ts +2 -2
- package/helper/i18n.d.ts.map +1 -1
- package/helper/i18n.js +5 -4
- package/helper/i18n.js.map +1 -1
- package/helper/logger.d.ts +1 -1
- package/helper/logger.d.ts.map +1 -1
- package/helper/logger.js +8 -5
- package/helper/logger.js.map +1 -1
- package/helper/mediaquery.js +1 -1
- package/helper/mediaquery.js.map +1 -1
- package/helper/ssr-shim.d.ts.map +1 -1
- package/helper/ssr-shim.js +2 -0
- package/helper/ssr-shim.js.map +1 -1
- package/helper/theme.d.ts +4 -5
- package/helper/theme.d.ts.map +1 -1
- package/helper/theme.js +3 -3
- package/helper/theme.js.map +1 -1
- package/index.d.ts +1 -3
- package/index.d.ts.map +1 -1
- package/index.js +1 -4
- package/index.js.map +1 -1
- package/lib/decorators.d.ts +9 -16
- package/lib/decorators.d.ts.map +1 -1
- package/lib/decorators.js +84 -71
- package/lib/decorators.js.map +1 -1
- package/lib/directive.d.ts +65 -0
- package/lib/directive.d.ts.map +1 -0
- package/lib/directive.js +58 -0
- package/lib/directive.js.map +1 -0
- package/lib/element.d.ts +10 -153
- package/lib/element.d.ts.map +1 -1
- package/lib/element.js +19 -480
- package/lib/element.js.map +1 -1
- package/lib/history.d.ts +67 -9
- package/lib/history.d.ts.map +1 -1
- package/lib/history.js +12 -12
- package/lib/history.js.map +1 -1
- package/lib/lit-html.d.ts +232 -0
- package/lib/lit-html.d.ts.map +1 -0
- package/lib/lit-html.js +987 -0
- package/lib/lit-html.js.map +1 -0
- package/lib/reactive.d.ts +123 -0
- package/lib/reactive.d.ts.map +1 -0
- package/lib/reactive.js +478 -0
- package/lib/reactive.js.map +1 -0
- package/lib/repeat.d.ts +126 -0
- package/lib/repeat.d.ts.map +1 -0
- package/lib/repeat.js +533 -0
- package/lib/repeat.js.map +1 -0
- package/lib/store.d.ts +13 -2
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +5 -4
- package/lib/store.js.map +1 -1
- package/lib/utils.d.ts +24 -15
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +25 -30
- package/lib/utils.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +8 -9
- package/src/dist.ts +6 -8
- package/src/elements/base/dialog.ts +2 -6
- package/src/elements/base/gesture.ts +35 -36
- package/src/elements/base/link.ts +20 -46
- package/src/elements/base/modal-factory.ts +14 -14
- package/src/elements/base/reflect.ts +2 -6
- package/src/elements/base/route.ts +22 -33
- package/src/elements/base/title.ts +4 -9
- package/src/elements/base/unsafe.ts +4 -9
- package/src/elements/base/use.ts +4 -11
- package/src/elements/link.ts +1 -1
- package/src/elements/route.ts +1 -1
- package/src/helper/hmr.ts +305 -0
- package/src/helper/i18n.ts +7 -7
- package/src/helper/logger.ts +9 -5
- package/src/helper/mediaquery.ts +1 -1
- package/src/helper/ssr-shim.ts +3 -0
- package/src/helper/theme.ts +6 -6
- package/src/index.ts +1 -5
- package/src/lib/decorators.ts +101 -100
- package/src/lib/directive.ts +137 -0
- package/src/lib/element.ts +26 -614
- package/src/lib/history.ts +14 -15
- package/src/lib/lit-html.ts +1269 -0
- package/src/lib/reactive.ts +596 -0
- package/src/lib/repeat.ts +580 -0
- package/src/lib/store.ts +5 -4
- package/src/lib/utils.ts +40 -46
- package/src/lib/version.ts +1 -1
- package/src/test/elements/link.test.ts +2 -3
- package/src/test/elements/reflect.test.ts +13 -3
- package/src/test/elements/title.test.ts +2 -3
- package/src/test/gem-element/advance.test.ts +22 -14
- package/src/test/gem-element/basic.test.ts +23 -11
- package/src/test/gem-element/decorators.test.ts +14 -17
- package/src/test/gem-element/multi.test.ts +20 -11
- package/src/test/history.test.ts +1 -2
- package/src/test/store.test.ts +3 -4
- package/src/test/utils.test.ts +12 -14
- package/src/test/utils.ts +22 -0
- package/lib/directives.d.ts +0 -4
- package/lib/directives.d.ts.map +0 -1
- package/lib/directives.js +0 -5
- package/lib/directives.js.map +0 -1
- package/src/lib/directives.ts +0 -6
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import type * as Lit from './lit-html';
|
|
2
|
+
import type { Store } from './store';
|
|
3
|
+
import { connect } from './store';
|
|
4
|
+
import { addListener, addMicrotask, createUpdater, GemError, isArrayChange, LinkedList, randomStr, raw } from './utils';
|
|
5
|
+
|
|
6
|
+
export let html = ((arr: TemplateStringsArray, ...args: any[]) =>
|
|
7
|
+
raw(arr, ...args.map((e) => (e instanceof Element ? e.outerHTML : e)))) as unknown as typeof Lit.html;
|
|
8
|
+
export let render = ((value: string, container: HTMLElement | ShadowRoot) =>
|
|
9
|
+
(container.innerHTML = value)) as unknown as typeof Lit.render;
|
|
10
|
+
|
|
11
|
+
export function setEngine(h: typeof Lit.html, r: typeof Lit.render) {
|
|
12
|
+
html = h;
|
|
13
|
+
render = r;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { assign, defineProperty } = Object;
|
|
17
|
+
|
|
18
|
+
// https://github.com/tc39/proposal-decorator-metadata
|
|
19
|
+
defineProperty(Symbol, 'metadata', { value: Symbol.for('Symbol.metadata') });
|
|
20
|
+
|
|
21
|
+
const execCallback = (fun: any) => typeof fun === 'function' && fun();
|
|
22
|
+
|
|
23
|
+
// 读取构造样式表,跨多个 gem 工作
|
|
24
|
+
export const SheetToken = Symbol.for('gem@sheetToken');
|
|
25
|
+
// 更新元素,跨多个 gem 工作
|
|
26
|
+
export const UpdateToken = Symbol.for('gem@update');
|
|
27
|
+
|
|
28
|
+
const BoundaryCSSState = 'gem-style-boundary';
|
|
29
|
+
|
|
30
|
+
/**@internal */
|
|
31
|
+
export const _RenderErrorEvent = 'gem@render-error';
|
|
32
|
+
|
|
33
|
+
// fix modal-factory type error
|
|
34
|
+
const updateTokenAlias = UpdateToken;
|
|
35
|
+
|
|
36
|
+
const rootStyleSheetInfo = new WeakMap<Document | ShadowRoot, Map<CSSStyleSheet, number>>();
|
|
37
|
+
const rootUpdateFnMap = new WeakMap<Map<CSSStyleSheet, number>, () => void>();
|
|
38
|
+
|
|
39
|
+
class GemCSSStyleSheet extends CSSStyleSheet {
|
|
40
|
+
#ele?: HTMLStyleElement;
|
|
41
|
+
|
|
42
|
+
cssText = '';
|
|
43
|
+
get element() {
|
|
44
|
+
if (!this.#ele) {
|
|
45
|
+
this.#ele = document.createElement('style');
|
|
46
|
+
this.#ele.textContent = this.cssText;
|
|
47
|
+
}
|
|
48
|
+
return this.#ele;
|
|
49
|
+
}
|
|
50
|
+
replaceSync(text: string) {
|
|
51
|
+
this.cssText = text;
|
|
52
|
+
if (this.#ele) this.#ele.textContent = text;
|
|
53
|
+
super.replaceSync(text);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class GemCSSSheet {
|
|
58
|
+
#content = '';
|
|
59
|
+
#media = '';
|
|
60
|
+
constructor(media = '') {
|
|
61
|
+
this.#media = media;
|
|
62
|
+
}
|
|
63
|
+
setContent(v: string) {
|
|
64
|
+
this.#content = v;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 不需要 GC
|
|
68
|
+
#record = new Map<any, GemCSSStyleSheet>();
|
|
69
|
+
#used = new Map<GemCSSStyleSheet, string>();
|
|
70
|
+
getStyle(host?: HTMLElement) {
|
|
71
|
+
const isLight = host && !(host as GemElement).internals?.shadowRoot;
|
|
72
|
+
|
|
73
|
+
// 对同一类 dom 只使用同一个样式表
|
|
74
|
+
const key = isLight ? host.constructor : this;
|
|
75
|
+
if (!this.#record.has(key)) {
|
|
76
|
+
const sheet = new GemCSSStyleSheet({ media: this.#media });
|
|
77
|
+
this.#record.set(key, sheet);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const sheet = this.#record.get(key)!;
|
|
81
|
+
|
|
82
|
+
// 只执行一次
|
|
83
|
+
if (!this.#used.has(sheet)) {
|
|
84
|
+
let style = this.#content;
|
|
85
|
+
let scope = '';
|
|
86
|
+
if (isLight) {
|
|
87
|
+
// light dom 嵌套时需要选择子元素
|
|
88
|
+
// `> *` 实际上是多范围?是否存在性能问题
|
|
89
|
+
scope = `@scope (${host.tagName}) to (:state(${BoundaryCSSState}) > *)`;
|
|
90
|
+
// 不能使用 @layer,两个 @layer 不能覆盖,只有顺序起作用
|
|
91
|
+
// 所以外部不能通过元素名称选择器来覆盖样式,除非样式在之前插入(会自动反转应用到尾部, see `appleCSSStyleSheet`)
|
|
92
|
+
style = `${scope}{ ${style} }`;
|
|
93
|
+
}
|
|
94
|
+
sheet.replaceSync(style);
|
|
95
|
+
this.#used.set(sheet, scope);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return sheet;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 一般用于主题更新
|
|
102
|
+
updateStyle() {
|
|
103
|
+
this.#used.forEach((scope, sheet) => {
|
|
104
|
+
sheet.replaceSync(scope ? `${scope}{${this.#content}}` : this.#content);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type Sheet<T> = { [P in keyof T]: string } & { [SheetToken]: GemCSSSheet };
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
*
|
|
113
|
+
* 创建 style sheet 用于 `@adoptedStyle`,不支持样式更新,只支持自定义 CSS 属性
|
|
114
|
+
*/
|
|
115
|
+
export function css<T extends Record<string, string>>(media: TemplateStringsArray, ...rest: any[]): Sheet<T>;
|
|
116
|
+
export function css<T extends Record<string, string>>(media: string, rules: T | string): Sheet<T>;
|
|
117
|
+
export function css<T extends Record<string, string>>(rules: T | string): Sheet<T>;
|
|
118
|
+
export function css<T extends Record<string, string>>(
|
|
119
|
+
mediaOrRules: T | string | TemplateStringsArray,
|
|
120
|
+
...rest: any[]
|
|
121
|
+
): Sheet<T> {
|
|
122
|
+
let media = '';
|
|
123
|
+
let rules: T | string = '';
|
|
124
|
+
|
|
125
|
+
const rulesValue = rest.at(0);
|
|
126
|
+
if (Array.isArray(mediaOrRules)) {
|
|
127
|
+
rules = raw(mediaOrRules as TemplateStringsArray, ...rest);
|
|
128
|
+
} else if (rulesValue !== undefined) {
|
|
129
|
+
media = mediaOrRules as string;
|
|
130
|
+
rules = rulesValue;
|
|
131
|
+
} else {
|
|
132
|
+
rules = mediaOrRules as string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const styleSheet = new GemCSSSheet(media);
|
|
136
|
+
const sheet: any = { [SheetToken]: styleSheet };
|
|
137
|
+
let style = '';
|
|
138
|
+
if (typeof rules === 'string') {
|
|
139
|
+
style = rules;
|
|
140
|
+
} else {
|
|
141
|
+
Object.keys(rules).forEach((key) => {
|
|
142
|
+
const isScope = key === '$';
|
|
143
|
+
// 对于已经有 `-` 的保留原始 key,支持覆盖修改
|
|
144
|
+
// :scope 下可以写嵌套样式 &:xxx,:host() 下不行(子内容可以)
|
|
145
|
+
sheet[key] = isScope || key.includes('-') ? key : `${key}-${randomStr()}`;
|
|
146
|
+
style += `${isScope ? ':where(:scope:not([hidden])),:host(:where(:not([hidden])))' : `.${sheet[key]}`} {${rules[key]}}`;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
styleSheet.setContent(style);
|
|
150
|
+
return sheet as Sheet<T>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// TODO: move to transform?
|
|
154
|
+
export const isSSR = typeof process !== 'undefined';
|
|
155
|
+
|
|
156
|
+
const updateStyleSheets = (map: Map<CSSStyleSheet, number>, sheets: CSSStyleSheet[], value: number) => {
|
|
157
|
+
let needUpdate = false;
|
|
158
|
+
sheets.forEach((e) => {
|
|
159
|
+
const count = map.get(e) || 0;
|
|
160
|
+
needUpdate ||= (value === 1 && count === 0) || (value === -1 && count === 1);
|
|
161
|
+
// count 为 0 时不立即删除,以便更新时识别是否外部样式
|
|
162
|
+
// 但是只要挂载 document 的样式表就不会被 GC 了, 是否要在闲时 GC?
|
|
163
|
+
map.set(e, count + value);
|
|
164
|
+
});
|
|
165
|
+
if (needUpdate) {
|
|
166
|
+
// 避免重复更新,例如 list 元素增删,全 light dom 时 document 更新
|
|
167
|
+
addMicrotask(rootUpdateFnMap.get(map)!, isSSR);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const appleCSSStyleSheet = (ele: HTMLElement, sheets: CSSStyleSheet[]) => {
|
|
172
|
+
const root = ele.getRootNode() as ShadowRoot | Document;
|
|
173
|
+
if (!rootStyleSheetInfo.has(root)) {
|
|
174
|
+
const map = new Map<CSSStyleSheet, number>();
|
|
175
|
+
rootStyleSheetInfo.set(root, map);
|
|
176
|
+
rootUpdateFnMap.set(map, () => {
|
|
177
|
+
// 先找到外部样式表
|
|
178
|
+
const newSheets = root.adoptedStyleSheets.filter((e) => !map.has(e));
|
|
179
|
+
map.forEach((count, sheet) => count && newSheets.push(sheet));
|
|
180
|
+
// 外层元素的样式要放到最后,以提升优先级,但是只考虑第一次出现样式表的位置
|
|
181
|
+
root.adoptedStyleSheets = newSheets.reverse();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const map = rootStyleSheetInfo.get(root)!;
|
|
186
|
+
|
|
187
|
+
updateStyleSheets(map, sheets, 1);
|
|
188
|
+
return () => updateStyleSheets(map, sheets, -1);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**必须使用在字段中,否则会读取到错误的实例 */
|
|
192
|
+
export let createState: <T>(initState: T) => ReturnType<typeof createUpdater<T>>;
|
|
193
|
+
|
|
194
|
+
type GetDepFun<T> = () => T;
|
|
195
|
+
type EffectCallback<T> = (depValues: T, oldDepValues?: T) => any;
|
|
196
|
+
type EffectItem<T> = {
|
|
197
|
+
callback: EffectCallback<T>;
|
|
198
|
+
initialized?: boolean;
|
|
199
|
+
fixed?: boolean; // 不需要清理,只会添加一次
|
|
200
|
+
values?: T;
|
|
201
|
+
getDep?: GetDepFun<T>;
|
|
202
|
+
preCallback?: any;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
function execEffectList(list?: EffectItem<any>[]) {
|
|
206
|
+
list?.forEach((effectItem) => {
|
|
207
|
+
const { callback, getDep, values, preCallback } = effectItem;
|
|
208
|
+
const newValues = getDep?.();
|
|
209
|
+
if (!getDep || !values || isArrayChange(values, newValues)) {
|
|
210
|
+
execCallback(preCallback);
|
|
211
|
+
effectItem.preCallback = callback(newValues, values);
|
|
212
|
+
effectItem.values = newValues;
|
|
213
|
+
// 更新时也要设置,避免异步的 `#initEffect` 重复执行
|
|
214
|
+
effectItem.initialized = true;
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function clearEffect(list: EffectItem<any>[]) {
|
|
220
|
+
return list.filter((e) => {
|
|
221
|
+
execCallback(e.preCallback);
|
|
222
|
+
e.initialized = false;
|
|
223
|
+
e.values = undefined;
|
|
224
|
+
return e.fixed;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
type Render = () => Lit.TemplateResult | null | undefined;
|
|
229
|
+
type RenderItem = { render: Render; condition?: () => boolean };
|
|
230
|
+
|
|
231
|
+
/**@internal */
|
|
232
|
+
export let _createTemplate: (ele: GemElement, item: RenderItem) => void;
|
|
233
|
+
|
|
234
|
+
export type Metadata = Partial<ShadowRootInit> & {
|
|
235
|
+
/** 内容可被外部样式化 */
|
|
236
|
+
penetrable?: boolean;
|
|
237
|
+
noBlocking?: boolean;
|
|
238
|
+
aria?: Partial<
|
|
239
|
+
ARIAMixin & {
|
|
240
|
+
/** 自动添加 tabIndex;支持 `disabled` 属性 */
|
|
241
|
+
focusable: boolean;
|
|
242
|
+
}
|
|
243
|
+
>;
|
|
244
|
+
// 实例化时使用到,DevTools 需要读取
|
|
245
|
+
observedStores?: Store<any>[];
|
|
246
|
+
adoptedStyleSheets?: Sheet<unknown>[];
|
|
247
|
+
// 以下静态字段仅供外部读取,没有实际作用
|
|
248
|
+
observedProperties?: string[];
|
|
249
|
+
observedAttributes?: string[];
|
|
250
|
+
definedEvents?: string[];
|
|
251
|
+
definedCSSStates?: string[];
|
|
252
|
+
definedParts?: string[];
|
|
253
|
+
definedSlots?: string[];
|
|
254
|
+
/**@deprecated GemDevtools 在使用*/
|
|
255
|
+
definedRefs?: string[];
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// global render task pool
|
|
259
|
+
const noBlockingTaskList = new LinkedList<() => void>();
|
|
260
|
+
const tick = (timeStamp = performance.now()) => {
|
|
261
|
+
if (performance.now() > timeStamp + 16) return requestAnimationFrame(tick);
|
|
262
|
+
const task = noBlockingTaskList.get();
|
|
263
|
+
if (task) {
|
|
264
|
+
task();
|
|
265
|
+
tick(timeStamp);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
noBlockingTaskList.addEventListener('start', () => addMicrotask(tick));
|
|
269
|
+
|
|
270
|
+
let currentConstructGemElement: GemElement;
|
|
271
|
+
|
|
272
|
+
export abstract class GemElement extends HTMLElement {
|
|
273
|
+
#renderRoot: HTMLElement | ShadowRoot;
|
|
274
|
+
#internals: ElementInternals & {
|
|
275
|
+
// 有别于 adoptedStyleSheets,用来定义单个实例样式
|
|
276
|
+
sheets: GemCSSStyleSheet[];
|
|
277
|
+
stateList: ReturnType<typeof createState<any>>[];
|
|
278
|
+
};
|
|
279
|
+
#effectList: EffectItem<any>[] = [];
|
|
280
|
+
#memoList: EffectItem<any>[] = [];
|
|
281
|
+
#renderList: RenderItem[] = [];
|
|
282
|
+
#isAppendReason?: boolean;
|
|
283
|
+
#isMounted?: boolean;
|
|
284
|
+
// not in constructor 的近似值
|
|
285
|
+
#notInCons?: boolean;
|
|
286
|
+
#rendering?: boolean;
|
|
287
|
+
#clearStyle?: (() => void) | undefined;
|
|
288
|
+
|
|
289
|
+
[updateTokenAlias]() {
|
|
290
|
+
// 避免 `connectedCallback` 中的 property 赋值造成多余更新
|
|
291
|
+
if (this.#isMounted) addMicrotask(this.#update);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
static {
|
|
295
|
+
createState = <T>(initState: T) => {
|
|
296
|
+
const ele = currentConstructGemElement;
|
|
297
|
+
const state = createUpdater(initState, (payload) => {
|
|
298
|
+
const effect = ele.#effectList.at(0);
|
|
299
|
+
// https://github.com/mantou132/gem/issues/203
|
|
300
|
+
if (ele.#isMounted && effect && !effect.initialized) {
|
|
301
|
+
throw new GemError(`Do't set state sync before insert the DOM`);
|
|
302
|
+
}
|
|
303
|
+
assign(state, payload);
|
|
304
|
+
// 避免无限刷新
|
|
305
|
+
// 挂载前 set state 不应该触发更新
|
|
306
|
+
if (!ele.#rendering && ele.#isMounted) addMicrotask(ele.#update);
|
|
307
|
+
});
|
|
308
|
+
ele.#internals.stateList.push(state);
|
|
309
|
+
return state;
|
|
310
|
+
};
|
|
311
|
+
_createTemplate = (ele, item) => ele.#renderList.push(item);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
constructor() {
|
|
315
|
+
super();
|
|
316
|
+
|
|
317
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
318
|
+
currentConstructGemElement = this;
|
|
319
|
+
|
|
320
|
+
const { mode, serializable, delegatesFocus, slotAssignment, aria } = this.#metadata;
|
|
321
|
+
const { focusable, ...internalsAria } = aria || {};
|
|
322
|
+
|
|
323
|
+
this.#renderRoot = !mode ? this : this.attachShadow({ mode, serializable, delegatesFocus, slotAssignment });
|
|
324
|
+
this.#internals = this.attachInternals() as GemElement['internals'];
|
|
325
|
+
this.#internals.stateList = [];
|
|
326
|
+
this.#internals.sheets = [];
|
|
327
|
+
|
|
328
|
+
assign(this.#internals, internalsAria);
|
|
329
|
+
|
|
330
|
+
// https://stackoverflow.com/questions/43836886/failed-to-construct-customelement-error-when-javascript-file-is-placed-in-head
|
|
331
|
+
// focusable 元素一般同时具备 disabled 属性
|
|
332
|
+
// 和原生元素行为保持一致,disabled 时不触发 click 事件
|
|
333
|
+
let hasInitTabIndex: boolean | undefined;
|
|
334
|
+
this.#effectList.push({
|
|
335
|
+
fixed: true,
|
|
336
|
+
getDep: () => [(this as any).disabled],
|
|
337
|
+
callback: ([disabled = false]) => {
|
|
338
|
+
if (hasInitTabIndex === undefined) hasInitTabIndex = this.hasAttribute('tabindex');
|
|
339
|
+
this.#internals.ariaDisabled = String(disabled);
|
|
340
|
+
if (focusable && !hasInitTabIndex) this.tabIndex = -Number(disabled);
|
|
341
|
+
if ((focusable || delegatesFocus) && disabled) {
|
|
342
|
+
return addListener(this, 'click', (e: Event) => e.isTrusted && e.stopImmediatePropagation(), {
|
|
343
|
+
capture: true,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
get #metadata(): Metadata {
|
|
351
|
+
return (this.constructor as any)[Symbol.metadata];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
get #renderItem() {
|
|
355
|
+
// 从末尾开始寻找,允许被覆盖
|
|
356
|
+
return this.#renderList.findLast((e) => !e.condition || e.condition());
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
get internals() {
|
|
360
|
+
return this.#internals;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* 每次更新完检查依赖,执行对应的副作用回调
|
|
365
|
+
* */
|
|
366
|
+
#execEffect = () => {
|
|
367
|
+
execEffectList(this.#effectList);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
#execMemo = () => {
|
|
371
|
+
execEffectList(this.#memoList);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* - 条件渲染
|
|
376
|
+
* - 没有 `render` 时显示子内容
|
|
377
|
+
* - 返回 `undefined` 时不会更新现有内容
|
|
378
|
+
* - 返回 `null` 时渲染空内容
|
|
379
|
+
* - 返回空的 TemplateResult 时渲染空的子内容
|
|
380
|
+
* */
|
|
381
|
+
#render = (item?: RenderItem) => {
|
|
382
|
+
try {
|
|
383
|
+
this.#rendering = true;
|
|
384
|
+
this.#execMemo();
|
|
385
|
+
const isLight = this.#renderRoot === this;
|
|
386
|
+
const temp = item ? item.render() : isLight ? undefined : html`<slot></slot>`;
|
|
387
|
+
if (temp === undefined) return;
|
|
388
|
+
render(
|
|
389
|
+
temp !== null
|
|
390
|
+
? temp
|
|
391
|
+
: isLight
|
|
392
|
+
? // prettier-ignore
|
|
393
|
+
html`<style>@scope{:scope{display:none !important}}</style>`
|
|
394
|
+
: // prettier-ignore
|
|
395
|
+
html`<style>:host{display:none !important}</style>`,
|
|
396
|
+
this.#renderRoot,
|
|
397
|
+
);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
this.dispatchEvent(new CustomEvent(_RenderErrorEvent, { bubbles: true, composed: true, detail: err }));
|
|
400
|
+
throw err;
|
|
401
|
+
} finally {
|
|
402
|
+
this.#rendering = false;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
#updateCallback = () => {
|
|
407
|
+
const item = this.#renderItem;
|
|
408
|
+
if (
|
|
409
|
+
this.#isMounted &&
|
|
410
|
+
// 当有渲染函数,但是没一个符合条件时跳过
|
|
411
|
+
(item || !this.#renderList.length)
|
|
412
|
+
) {
|
|
413
|
+
this.#render(item);
|
|
414
|
+
addMicrotask(this.#execEffect);
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
#update = () => {
|
|
419
|
+
if (this.#metadata.noBlocking) {
|
|
420
|
+
noBlockingTaskList.add(this.#updateCallback);
|
|
421
|
+
} else {
|
|
422
|
+
this.#updateCallback();
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
#prepareStyle = () => {
|
|
427
|
+
const { adoptedStyleSheets = [] } = this.#metadata;
|
|
428
|
+
const { shadowRoot, sheets } = this.#internals;
|
|
429
|
+
|
|
430
|
+
const clsSheets = adoptedStyleSheets.map((item) => item[SheetToken].getStyle(this));
|
|
431
|
+
if (shadowRoot) {
|
|
432
|
+
shadowRoot.adoptedStyleSheets = clsSheets.concat(sheets);
|
|
433
|
+
} else {
|
|
434
|
+
this.prepend(...sheets.map((e) => e.element));
|
|
435
|
+
return appleCSSStyleSheet(this, clsSheets);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 元素挂载后执行还未初始化的副作用
|
|
441
|
+
* */
|
|
442
|
+
#initEffect = () => {
|
|
443
|
+
this.#effectList?.forEach((effectItem) => {
|
|
444
|
+
const { callback, getDep, initialized } = effectItem;
|
|
445
|
+
if (!initialized) {
|
|
446
|
+
effectItem.values = getDep?.();
|
|
447
|
+
effectItem.preCallback = callback(effectItem.values);
|
|
448
|
+
effectItem.initialized = true;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
/** @lifecycle 将来会彻底移除,现在做兼容实现*/ willMount?(): void | Promise<void>;
|
|
454
|
+
/** @lifecycle 将来会彻底移除,现在做兼容实现*/ mounted?(): void | (() => void) | Promise<void>;
|
|
455
|
+
/** @lifecycle 将来会彻底移除,现在做兼容实现*/ updated?(): void | Promise<void>;
|
|
456
|
+
/** @lifecycle 将来会彻底移除,现在做兼容实现*/ unmounted?(): void | Promise<void>;
|
|
457
|
+
/** @lifecycle 将来会彻底移除,现在做兼容实现*/ shouldUpdate?(): boolean;
|
|
458
|
+
/** @lifecycle 将来会彻底移除,现在做兼容实现*/ render?(): Lit.TemplateResult | null | undefined;
|
|
459
|
+
#compat = () => {
|
|
460
|
+
if (this.willMount) this.memo(this.willMount.bind(this), () => []);
|
|
461
|
+
if (this.mounted) this.effect(this.mounted.bind(this), () => []);
|
|
462
|
+
if (this.updated) this.effect((_, old) => old && this.updated!.apply(this));
|
|
463
|
+
if (this.unmounted)
|
|
464
|
+
this.effect(
|
|
465
|
+
() => this.unmounted!.bind(this),
|
|
466
|
+
() => [],
|
|
467
|
+
);
|
|
468
|
+
if (!this.#renderList.length && this.render)
|
|
469
|
+
this.#renderList.push({ render: this.render.bind(this), condition: this.shouldUpdate?.bind(this) });
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
#disconnectStore?: (() => void)[];
|
|
473
|
+
#connectedCallback = async () => {
|
|
474
|
+
if (this.#isAppendReason) {
|
|
475
|
+
this.#isAppendReason = false;
|
|
476
|
+
// https://github.com/mantou132/gem/issues/202
|
|
477
|
+
this.#clearStyle?.();
|
|
478
|
+
this.#clearStyle = this.#prepareStyle();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
this.#notInCons = true;
|
|
482
|
+
this.#compat();
|
|
483
|
+
|
|
484
|
+
const { observedStores, mode, penetrable } = this.#metadata;
|
|
485
|
+
|
|
486
|
+
// 有渲染函数的 light dom 应该添加边界,防止内容被外部样式化
|
|
487
|
+
// 如果渲染内容需要应用外部样式,需要手动 `delete` 边界
|
|
488
|
+
if (!mode && this.#renderList.length && !penetrable) this.#internals.states.add(BoundaryCSSState);
|
|
489
|
+
|
|
490
|
+
this.#disconnectStore = observedStores?.map((store) => connect(store, this.#update));
|
|
491
|
+
this.#render(this.#renderItem);
|
|
492
|
+
this.#isMounted = true;
|
|
493
|
+
this.#clearStyle = this.#prepareStyle();
|
|
494
|
+
// 等待所有元素的样式被应用,再执行回调
|
|
495
|
+
// 这让 mounted 和 effect 回调和其他回调一样保持一样的异步行为
|
|
496
|
+
addMicrotask(this.#initEffect);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* @private
|
|
501
|
+
* @final
|
|
502
|
+
* use `@mounted`
|
|
503
|
+
*/
|
|
504
|
+
connectedCallback(): Unique {
|
|
505
|
+
if (this.#metadata.noBlocking) {
|
|
506
|
+
noBlockingTaskList.add(this.#connectedCallback);
|
|
507
|
+
} else {
|
|
508
|
+
this.#connectedCallback();
|
|
509
|
+
}
|
|
510
|
+
return undefined as unknown as Unique;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* @private
|
|
515
|
+
* @final
|
|
516
|
+
* use `@unmounted`
|
|
517
|
+
*/
|
|
518
|
+
disconnectedCallback(): Unique {
|
|
519
|
+
if (this.isConnected) {
|
|
520
|
+
this.#isAppendReason = true;
|
|
521
|
+
return undefined as unknown as Unique;
|
|
522
|
+
}
|
|
523
|
+
noBlockingTaskList.delete(this.#connectedCallback);
|
|
524
|
+
noBlockingTaskList.delete(this.#updateCallback);
|
|
525
|
+
this.#isMounted = false;
|
|
526
|
+
this.#disconnectStore?.forEach((disconnect) => disconnect());
|
|
527
|
+
// 是否要异步执行回调?
|
|
528
|
+
this.#effectList = clearEffect(this.#effectList);
|
|
529
|
+
this.#memoList = clearEffect(this.#memoList);
|
|
530
|
+
this.#clearStyle?.();
|
|
531
|
+
return undefined as unknown as Unique;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* @helper
|
|
536
|
+
* 记录副作用回调和值;
|
|
537
|
+
* 回调到返回值如果是函数将再卸载时执行;
|
|
538
|
+
* 第一次执行时 `oldDeps` 为空;
|
|
539
|
+
*
|
|
540
|
+
* ```js
|
|
541
|
+
* class App extends GemElement {
|
|
542
|
+
* mounted() {
|
|
543
|
+
* this.effect(callback, () => [this.attrName]);
|
|
544
|
+
* }
|
|
545
|
+
* }
|
|
546
|
+
* ```
|
|
547
|
+
* */
|
|
548
|
+
effect = <K = any[] | undefined>(callback: EffectCallback<K>, getDep?: K extends any[] ? () => K : undefined) => {
|
|
549
|
+
const effectItem: EffectItem<K> = {
|
|
550
|
+
callback,
|
|
551
|
+
getDep,
|
|
552
|
+
initialized: this.#isMounted,
|
|
553
|
+
fixed: !this.#notInCons,
|
|
554
|
+
};
|
|
555
|
+
// 已挂载时立即执行副作用,未挂载时等挂载后执行
|
|
556
|
+
if (this.#isMounted) {
|
|
557
|
+
effectItem.values = getDep?.() as K;
|
|
558
|
+
effectItem.preCallback = callback(effectItem.values);
|
|
559
|
+
}
|
|
560
|
+
this.#effectList.push(effectItem);
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* @helper
|
|
565
|
+
* 在 `render` 前执行回调;
|
|
566
|
+
* 和 `effect` 一样接受依赖数组参数,在 `constructor` 中使用;
|
|
567
|
+
* 第一次执行时 `oldDeps` 为空;
|
|
568
|
+
*
|
|
569
|
+
* ```js
|
|
570
|
+
* class App extends GemElement {
|
|
571
|
+
* constructor() {
|
|
572
|
+
* super();
|
|
573
|
+
* this.memo(() => {
|
|
574
|
+
* this.a = exec(this.attrName);
|
|
575
|
+
* }), () => [this.attrName]);
|
|
576
|
+
* }
|
|
577
|
+
* }
|
|
578
|
+
* ```
|
|
579
|
+
* */
|
|
580
|
+
memo = <K = any[] | undefined>(callback: EffectCallback<K>, getDep?: K extends any[] ? () => K : undefined) => {
|
|
581
|
+
this.#memoList.push({
|
|
582
|
+
callback,
|
|
583
|
+
getDep,
|
|
584
|
+
fixed: !this.#notInCons,
|
|
585
|
+
});
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* @helper
|
|
590
|
+
* async
|
|
591
|
+
*/
|
|
592
|
+
update = () => addMicrotask(this.#update);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// 用来禁止 connectCallback 方法被覆盖而破坏 GemElement 的功能
|
|
596
|
+
type Unique = typeof UpdateToken;
|