@wsxjs/wsx-core 0.0.7 → 0.0.8
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/index.js +297 -424
- package/dist/index.mjs +295 -417
- package/package.json +1 -1
- package/src/base-component.ts +239 -0
- package/src/index.ts +2 -4
- package/src/light-component.ts +13 -163
- package/src/reactive-decorator.ts +105 -0
- package/src/web-component.ts +157 -110
- package/types/index.d.ts +2 -2
- package/src/reactive-component.ts +0 -306
package/src/web-component.ts
CHANGED
|
@@ -10,43 +10,42 @@
|
|
|
10
10
|
|
|
11
11
|
import { h, type JSXChildren } from "./jsx-factory";
|
|
12
12
|
import { StyleManager } from "./styles/style-manager";
|
|
13
|
+
import { BaseComponent, type BaseComponentConfig } from "./base-component";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Web Component 配置接口
|
|
16
17
|
*/
|
|
17
|
-
export interface WebComponentConfig {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export interface WebComponentConfig extends BaseComponentConfig {
|
|
19
|
+
preserveFocus?: boolean; // 是否在重渲染时保持焦点
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Type for focus data saved during rerender
|
|
24
|
+
*/
|
|
25
|
+
interface FocusData {
|
|
26
|
+
tagName: string;
|
|
27
|
+
className: string;
|
|
28
|
+
value?: string;
|
|
29
|
+
selectionStart?: number;
|
|
30
|
+
selectionEnd?: number;
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
/**
|
|
24
34
|
* 通用 WSX Web Component 基础抽象类
|
|
25
35
|
*/
|
|
26
|
-
export abstract class WebComponent extends
|
|
36
|
+
export abstract class WebComponent extends BaseComponent {
|
|
27
37
|
declare shadowRoot: ShadowRoot;
|
|
28
|
-
protected config
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 子类应该重写这个方法来定义观察的属性
|
|
33
|
-
* @returns 要观察的属性名数组
|
|
34
|
-
*/
|
|
35
|
-
static get observedAttributes(): string[] {
|
|
36
|
-
return [];
|
|
37
|
-
}
|
|
38
|
+
protected config!: WebComponentConfig; // Initialized by BaseComponent constructor
|
|
39
|
+
private _preserveFocus: boolean = true;
|
|
38
40
|
|
|
39
41
|
constructor(config: WebComponentConfig = {}) {
|
|
40
|
-
super();
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
super(config);
|
|
43
|
+
// BaseComponent already created this.config with getter for styles
|
|
44
|
+
// Just update preserveFocus property
|
|
45
|
+
this._preserveFocus = config.preserveFocus ?? true;
|
|
43
46
|
this.attachShadow({ mode: "open" });
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
if (config.styles) {
|
|
47
|
-
const styleName = config.styleName || this.constructor.name;
|
|
48
|
-
StyleManager.applyStyles(this.shadowRoot, styleName, config.styles);
|
|
49
|
-
}
|
|
47
|
+
// Styles are applied in connectedCallback for consistency with LightComponent
|
|
48
|
+
// and to ensure all class properties (including getters) are initialized
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
/**
|
|
@@ -62,6 +61,15 @@ export abstract class WebComponent extends HTMLElement {
|
|
|
62
61
|
connectedCallback(): void {
|
|
63
62
|
this.connected = true;
|
|
64
63
|
try {
|
|
64
|
+
// 应用CSS样式到Shadow DOM
|
|
65
|
+
// Using method call ensures styles are available when accessed
|
|
66
|
+
// This is consistent with LightComponent and avoids execution order issues
|
|
67
|
+
const stylesToApply = this._getAutoStyles?.() || this.config.styles;
|
|
68
|
+
if (stylesToApply) {
|
|
69
|
+
const styleName = this.config.styleName || this.constructor.name;
|
|
70
|
+
StyleManager.applyStyles(this.shadowRoot, styleName, stylesToApply);
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
// 渲染JSX内容到Shadow DOM
|
|
66
74
|
const content = this.render();
|
|
67
75
|
this.shadowRoot.appendChild(content);
|
|
@@ -78,31 +86,10 @@ export abstract class WebComponent extends HTMLElement {
|
|
|
78
86
|
* Web Component生命周期:从DOM断开
|
|
79
87
|
*/
|
|
80
88
|
disconnectedCallback(): void {
|
|
89
|
+
this.connected = false;
|
|
81
90
|
this.onDisconnected?.();
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
/**
|
|
85
|
-
* Web Component生命周期:属性变化
|
|
86
|
-
*/
|
|
87
|
-
attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
|
|
88
|
-
this.onAttributeChanged?.(name, oldValue, newValue);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* 可选生命周期钩子:组件已连接
|
|
93
|
-
*/
|
|
94
|
-
protected onConnected?(): void;
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 可选生命周期钩子:组件已断开
|
|
98
|
-
*/
|
|
99
|
-
protected onDisconnected?(): void;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* 可选生命周期钩子:属性已更改
|
|
103
|
-
*/
|
|
104
|
-
protected onAttributeChanged?(name: string, oldValue: string, newValue: string): void;
|
|
105
|
-
|
|
106
93
|
/**
|
|
107
94
|
* 查找Shadow DOM内的元素
|
|
108
95
|
*
|
|
@@ -133,6 +120,14 @@ export abstract class WebComponent extends HTMLElement {
|
|
|
133
120
|
);
|
|
134
121
|
return;
|
|
135
122
|
}
|
|
123
|
+
|
|
124
|
+
// 保存焦点状态(如果启用)
|
|
125
|
+
let focusData: FocusData | null = null;
|
|
126
|
+
if (this._preserveFocus && this.shadowRoot) {
|
|
127
|
+
const activeElement = this.shadowRoot.activeElement;
|
|
128
|
+
focusData = this.saveFocusState(activeElement);
|
|
129
|
+
}
|
|
130
|
+
|
|
136
131
|
// 保存当前的 adopted stylesheets (jsdom may not support this)
|
|
137
132
|
const adoptedStyleSheets = this.shadowRoot.adoptedStyleSheets || [];
|
|
138
133
|
|
|
@@ -145,9 +140,13 @@ export abstract class WebComponent extends HTMLElement {
|
|
|
145
140
|
}
|
|
146
141
|
|
|
147
142
|
// 只有在没有 adopted stylesheets 时才重新应用样式
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
143
|
+
// Check both _autoStyles getter and config.styles getter
|
|
144
|
+
if (adoptedStyleSheets.length === 0) {
|
|
145
|
+
const stylesToApply = this._autoStyles || this.config.styles;
|
|
146
|
+
if (stylesToApply) {
|
|
147
|
+
const styleName = this.config.styleName || this.constructor.name;
|
|
148
|
+
StyleManager.applyStyles(this.shadowRoot, styleName, stylesToApply);
|
|
149
|
+
}
|
|
151
150
|
}
|
|
152
151
|
|
|
153
152
|
// 重新渲染JSX
|
|
@@ -158,6 +157,115 @@ export abstract class WebComponent extends HTMLElement {
|
|
|
158
157
|
console.error(`[${this.constructor.name}] Error in rerender:`, error);
|
|
159
158
|
this.renderError(error);
|
|
160
159
|
}
|
|
160
|
+
|
|
161
|
+
// 恢复焦点状态
|
|
162
|
+
if (this._preserveFocus && focusData && this.shadowRoot) {
|
|
163
|
+
this.restoreFocusState(focusData);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 保存焦点状态
|
|
169
|
+
*/
|
|
170
|
+
private saveFocusState(activeElement: Element | null): FocusData | null {
|
|
171
|
+
if (!activeElement) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const focusData: FocusData = {
|
|
176
|
+
tagName: activeElement.tagName.toLowerCase(),
|
|
177
|
+
className: activeElement.className,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Save selection/cursor position
|
|
181
|
+
if (activeElement.hasAttribute("contenteditable")) {
|
|
182
|
+
const selection = window.getSelection();
|
|
183
|
+
if (selection && selection.rangeCount > 0) {
|
|
184
|
+
const range = selection.getRangeAt(0);
|
|
185
|
+
focusData.selectionStart = range.startOffset;
|
|
186
|
+
focusData.selectionEnd = range.endOffset;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Save input/select element state
|
|
191
|
+
if (
|
|
192
|
+
activeElement instanceof HTMLInputElement ||
|
|
193
|
+
activeElement instanceof HTMLSelectElement
|
|
194
|
+
) {
|
|
195
|
+
focusData.value = activeElement.value;
|
|
196
|
+
if ("selectionStart" in activeElement) {
|
|
197
|
+
focusData.selectionStart = activeElement.selectionStart ?? undefined;
|
|
198
|
+
focusData.selectionEnd = activeElement.selectionEnd ?? undefined;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return focusData;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 恢复焦点状态
|
|
207
|
+
*/
|
|
208
|
+
private restoreFocusState(focusData: FocusData): void {
|
|
209
|
+
if (!focusData) return;
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
let targetElement: Element | null = null;
|
|
213
|
+
|
|
214
|
+
// Try to find by className first (most specific)
|
|
215
|
+
if (focusData.className) {
|
|
216
|
+
targetElement = this.shadowRoot.querySelector(
|
|
217
|
+
`.${focusData.className.split(" ")[0]}`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Fallback: find by tag name
|
|
222
|
+
if (!targetElement) {
|
|
223
|
+
targetElement = this.shadowRoot.querySelector(focusData.tagName);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (targetElement) {
|
|
227
|
+
// Restore focus - prevent page scroll
|
|
228
|
+
(targetElement as HTMLElement).focus({ preventScroll: true });
|
|
229
|
+
|
|
230
|
+
// Restore selection/cursor position
|
|
231
|
+
if (focusData.selectionStart !== undefined) {
|
|
232
|
+
if (targetElement instanceof HTMLInputElement) {
|
|
233
|
+
targetElement.setSelectionRange(
|
|
234
|
+
focusData.selectionStart,
|
|
235
|
+
focusData.selectionEnd ?? focusData.selectionStart
|
|
236
|
+
);
|
|
237
|
+
} else if (targetElement instanceof HTMLSelectElement) {
|
|
238
|
+
targetElement.value = focusData.value ?? "";
|
|
239
|
+
} else if (targetElement.hasAttribute("contenteditable")) {
|
|
240
|
+
this.setCursorPosition(targetElement, focusData.selectionStart);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
// Silently handle focus restoration failure
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 设置光标位置
|
|
251
|
+
*/
|
|
252
|
+
private setCursorPosition(element: Element, position: number): void {
|
|
253
|
+
try {
|
|
254
|
+
const selection = window.getSelection();
|
|
255
|
+
if (selection) {
|
|
256
|
+
const range = document.createRange();
|
|
257
|
+
const textNode = element.childNodes[0];
|
|
258
|
+
if (textNode) {
|
|
259
|
+
const maxPos = Math.min(position, textNode.textContent?.length || 0);
|
|
260
|
+
range.setStart(textNode, maxPos);
|
|
261
|
+
range.setEnd(textNode, maxPos);
|
|
262
|
+
selection.removeAllRanges();
|
|
263
|
+
selection.addRange(range);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
// Silently handle cursor position failure
|
|
268
|
+
}
|
|
161
269
|
}
|
|
162
270
|
|
|
163
271
|
/**
|
|
@@ -182,67 +290,6 @@ export abstract class WebComponent extends HTMLElement {
|
|
|
182
290
|
|
|
183
291
|
this.shadowRoot.appendChild(errorElement);
|
|
184
292
|
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* 获取配置值
|
|
188
|
-
*
|
|
189
|
-
* @param key - 配置键
|
|
190
|
-
* @param defaultValue - 默认值
|
|
191
|
-
* @returns 配置值
|
|
192
|
-
*/
|
|
193
|
-
protected getConfig<T>(key: string, defaultValue?: T): T {
|
|
194
|
-
return (this.config[key] as T) ?? (defaultValue as T);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* 设置配置值
|
|
199
|
-
*
|
|
200
|
-
* @param key - 配置键
|
|
201
|
-
* @param value - 配置值
|
|
202
|
-
*/
|
|
203
|
-
protected setConfig(key: string, value: unknown): void {
|
|
204
|
-
this.config[key] = value;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* 获取属性值
|
|
209
|
-
*
|
|
210
|
-
* @param name - 属性名
|
|
211
|
-
* @param defaultValue - 默认值
|
|
212
|
-
* @returns 属性值
|
|
213
|
-
*/
|
|
214
|
-
protected getAttr(name: string, defaultValue = ""): string {
|
|
215
|
-
return this.getAttribute(name) || defaultValue;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* 设置属性值
|
|
220
|
-
*
|
|
221
|
-
* @param name - 属性名
|
|
222
|
-
* @param value - 属性值
|
|
223
|
-
*/
|
|
224
|
-
protected setAttr(name: string, value: string): void {
|
|
225
|
-
this.setAttribute(name, value);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* 移除属性
|
|
230
|
-
*
|
|
231
|
-
* @param name - 属性名
|
|
232
|
-
*/
|
|
233
|
-
protected removeAttr(name: string): void {
|
|
234
|
-
this.removeAttribute(name);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* 检查是否有属性
|
|
239
|
-
*
|
|
240
|
-
* @param name - 属性名
|
|
241
|
-
* @returns 是否存在
|
|
242
|
-
*/
|
|
243
|
-
protected hasAttr(name: string): boolean {
|
|
244
|
-
return this.hasAttribute(name);
|
|
245
|
-
}
|
|
246
293
|
}
|
|
247
294
|
|
|
248
295
|
// 导出JSX助手
|
package/types/index.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export type { JSXChildren } from "../src/jsx-factory";
|
|
|
10
10
|
// 导出 WebComponent 类和配置
|
|
11
11
|
export { WebComponent, type WebComponentConfig } from "../src/web-component";
|
|
12
12
|
|
|
13
|
-
//
|
|
14
|
-
export {
|
|
13
|
+
// 导出响应式装饰器 (@state)
|
|
14
|
+
export { state } from "../src/reactive-decorator";
|
|
15
15
|
|
|
16
16
|
// 导出 LightComponent 类和配置
|
|
17
17
|
export { LightComponent, type LightComponentConfig } from "../src/light-component";
|
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 响应式 WSX Web Component
|
|
3
|
-
*
|
|
4
|
-
* 扩展基础 WebComponent,提供响应式状态支持
|
|
5
|
-
* 遵循 WSX 理念:可选使用,不破坏现有组件
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { WebComponent, type WebComponentConfig } from "./web-component";
|
|
9
|
-
import { reactive, createState, reactiveWithDebug } from "./utils/reactive";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 响应式 WebComponent 配置
|
|
13
|
-
*/
|
|
14
|
-
export interface ReactiveWebComponentConfig extends WebComponentConfig {
|
|
15
|
-
/** 是否启用响应式调试模式 */
|
|
16
|
-
debug?: boolean;
|
|
17
|
-
/** 是否启用自动焦点保持 */
|
|
18
|
-
preserveFocus?: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 响应式 WebComponent 基类
|
|
23
|
-
*
|
|
24
|
-
* 提供响应式状态管理能力,同时保持与标准 WebComponent 的完全兼容
|
|
25
|
-
*/
|
|
26
|
-
export abstract class ReactiveWebComponent extends WebComponent {
|
|
27
|
-
private _isDebugEnabled: boolean = false;
|
|
28
|
-
private _preserveFocus: boolean = true;
|
|
29
|
-
private _reactiveStates = new Map<string, any>();
|
|
30
|
-
|
|
31
|
-
constructor(config: ReactiveWebComponentConfig = {}) {
|
|
32
|
-
super(config);
|
|
33
|
-
|
|
34
|
-
this._isDebugEnabled = config.debug ?? false;
|
|
35
|
-
this._preserveFocus = config.preserveFocus ?? true;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 创建响应式对象
|
|
40
|
-
*
|
|
41
|
-
* @param obj 要变为响应式的对象
|
|
42
|
-
* @param debugName 调试名称(可选)
|
|
43
|
-
* @returns 响应式代理对象
|
|
44
|
-
*/
|
|
45
|
-
protected reactive<T extends object>(obj: T, debugName?: string): T {
|
|
46
|
-
const reactiveFn = this._isDebugEnabled ? reactiveWithDebug : reactive;
|
|
47
|
-
const name = debugName || `${this.constructor.name}.reactive`;
|
|
48
|
-
|
|
49
|
-
return this._isDebugEnabled
|
|
50
|
-
? reactiveFn(obj, () => this.scheduleRerender(), name)
|
|
51
|
-
: reactiveFn(obj, () => this.scheduleRerender());
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 创建响应式状态
|
|
56
|
-
*
|
|
57
|
-
* @param key 状态标识符
|
|
58
|
-
* @param initialValue 初始值
|
|
59
|
-
* @returns [getter, setter] 元组
|
|
60
|
-
*/
|
|
61
|
-
protected useState<T>(
|
|
62
|
-
key: string,
|
|
63
|
-
initialValue: T
|
|
64
|
-
): [() => T, (value: T | ((prev: T) => T)) => void] {
|
|
65
|
-
if (!this._reactiveStates.has(key)) {
|
|
66
|
-
const [getter, setter] = createState(initialValue, () => this.scheduleRerender());
|
|
67
|
-
this._reactiveStates.set(key, { getter, setter });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const state = this._reactiveStates.get(key);
|
|
71
|
-
return [state.getter, state.setter];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* 调度重渲染
|
|
76
|
-
* 这个方法被响应式系统调用,开发者通常不需要直接调用
|
|
77
|
-
*/
|
|
78
|
-
protected scheduleRerender(): void {
|
|
79
|
-
// 确保组件已连接到 DOM
|
|
80
|
-
if (this.connected) {
|
|
81
|
-
this.rerender();
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 重写 rerender 方法以支持焦点保持
|
|
87
|
-
*/
|
|
88
|
-
protected rerender(): void {
|
|
89
|
-
if (!this.connected) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
let focusData: any = null;
|
|
94
|
-
|
|
95
|
-
// 保存焦点状态(如果启用)
|
|
96
|
-
if (this._preserveFocus) {
|
|
97
|
-
const activeElement = this.shadowRoot.activeElement;
|
|
98
|
-
focusData = this.saveFocusState(activeElement);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// 调用父类的重渲染逻辑
|
|
102
|
-
super.rerender();
|
|
103
|
-
|
|
104
|
-
// 恢复焦点状态(如果启用)- 立即同步执行避免闪烁
|
|
105
|
-
if (this._preserveFocus && focusData) {
|
|
106
|
-
this.restoreFocusState(focusData);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* 保存焦点状态
|
|
112
|
-
*/
|
|
113
|
-
private saveFocusState(activeElement: Element | null): any {
|
|
114
|
-
if (!activeElement) {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 只保存可编辑元素的状态
|
|
119
|
-
const focusData: any = {
|
|
120
|
-
tagName: activeElement.tagName.toLowerCase(),
|
|
121
|
-
className: activeElement.className,
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// 保存选择/光标位置
|
|
125
|
-
if (activeElement.hasAttribute("contenteditable")) {
|
|
126
|
-
const selection = window.getSelection();
|
|
127
|
-
if (selection && selection.rangeCount > 0) {
|
|
128
|
-
const range = selection.getRangeAt(0);
|
|
129
|
-
focusData.selectionStart = range.startOffset;
|
|
130
|
-
focusData.selectionEnd = range.endOffset;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 保存输入/选择元素的状态
|
|
135
|
-
if (
|
|
136
|
-
activeElement instanceof HTMLInputElement ||
|
|
137
|
-
activeElement instanceof HTMLSelectElement
|
|
138
|
-
) {
|
|
139
|
-
focusData.value = activeElement.value;
|
|
140
|
-
if ("selectionStart" in activeElement) {
|
|
141
|
-
focusData.selectionStart = activeElement.selectionStart;
|
|
142
|
-
focusData.selectionEnd = activeElement.selectionEnd;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return focusData;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* 恢复焦点状态
|
|
151
|
-
*/
|
|
152
|
-
private restoreFocusState(focusData: any): void {
|
|
153
|
-
if (!focusData) return;
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
// 查找要恢复焦点的元素
|
|
157
|
-
let targetElement: Element | null = null;
|
|
158
|
-
|
|
159
|
-
// 首先尝试通过类名查找(最具体)
|
|
160
|
-
if (focusData.className) {
|
|
161
|
-
targetElement = this.shadowRoot.querySelector(
|
|
162
|
-
`.${focusData.className.split(" ")[0]}`
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// 备用方案:通过标签名查找
|
|
167
|
-
if (!targetElement) {
|
|
168
|
-
targetElement = this.shadowRoot.querySelector(focusData.tagName);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (targetElement) {
|
|
172
|
-
// 恢复焦点 - 防止页面滚动
|
|
173
|
-
(targetElement as HTMLElement).focus({ preventScroll: true });
|
|
174
|
-
|
|
175
|
-
// 恢复选择/光标位置
|
|
176
|
-
if (focusData.selectionStart !== undefined) {
|
|
177
|
-
if (targetElement instanceof HTMLInputElement) {
|
|
178
|
-
targetElement.setSelectionRange(
|
|
179
|
-
focusData.selectionStart,
|
|
180
|
-
focusData.selectionEnd
|
|
181
|
-
);
|
|
182
|
-
} else if (targetElement instanceof HTMLSelectElement) {
|
|
183
|
-
targetElement.value = focusData.value;
|
|
184
|
-
} else if (targetElement.hasAttribute("contenteditable")) {
|
|
185
|
-
this.setCursorPosition(targetElement, focusData.selectionStart);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
} catch {
|
|
190
|
-
// 静默处理焦点恢复失败,不应该影响正常渲染
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* 设置光标位置
|
|
196
|
-
*/
|
|
197
|
-
private setCursorPosition(element: Element, position: number): void {
|
|
198
|
-
try {
|
|
199
|
-
const selection = window.getSelection();
|
|
200
|
-
if (selection) {
|
|
201
|
-
const range = document.createRange();
|
|
202
|
-
const textNode = element.childNodes[0];
|
|
203
|
-
if (textNode) {
|
|
204
|
-
const maxPos = Math.min(position, textNode.textContent?.length || 0);
|
|
205
|
-
range.setStart(textNode, maxPos);
|
|
206
|
-
range.setEnd(textNode, maxPos);
|
|
207
|
-
selection.removeAllRanges();
|
|
208
|
-
selection.addRange(range);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
} catch {
|
|
212
|
-
// 静默处理,焦点恢复失败不应该阻止渲染
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* 获取所有响应式状态的快照(用于调试)
|
|
218
|
-
*/
|
|
219
|
-
protected getStateSnapshot(): Record<string, any> {
|
|
220
|
-
const snapshot: Record<string, any> = {};
|
|
221
|
-
|
|
222
|
-
this._reactiveStates.forEach((state, key) => {
|
|
223
|
-
snapshot[key] = state.getter();
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
return snapshot;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* 清理响应式状态(组件销毁时)
|
|
231
|
-
*/
|
|
232
|
-
protected cleanupReactiveStates(): void {
|
|
233
|
-
this._reactiveStates.clear();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* 重写 disconnectedCallback 以清理状态
|
|
238
|
-
*/
|
|
239
|
-
disconnectedCallback(): void {
|
|
240
|
-
super.disconnectedCallback();
|
|
241
|
-
this.cleanupReactiveStates();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* 启用调试模式
|
|
246
|
-
*/
|
|
247
|
-
protected enableDebug(): void {
|
|
248
|
-
this._isDebugEnabled = true;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* 禁用调试模式
|
|
253
|
-
*/
|
|
254
|
-
protected disableDebug(): void {
|
|
255
|
-
this._isDebugEnabled = false;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* 装饰器:自动使组件变为响应式
|
|
261
|
-
*
|
|
262
|
-
* @param debugMode 是否启用调试模式
|
|
263
|
-
*/
|
|
264
|
-
export function makeReactive(_debugMode: boolean = false) {
|
|
265
|
-
return function <T extends new (...args: any[]) => WebComponent>(constructor: T) {
|
|
266
|
-
return class ReactiveComponent extends constructor {
|
|
267
|
-
constructor(...args: any[]) {
|
|
268
|
-
super(...args);
|
|
269
|
-
|
|
270
|
-
// 如果不是 ReactiveWebComponent 的实例,则混入响应式能力
|
|
271
|
-
if (!(this instanceof ReactiveWebComponent)) {
|
|
272
|
-
// 添加响应式方法
|
|
273
|
-
(this as any).reactive = function <U extends object>(obj: U): U {
|
|
274
|
-
return reactive(obj, () => (this as any).rerender());
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
render(): HTMLElement {
|
|
280
|
-
// 抽象方法必须由子类实现
|
|
281
|
-
throw new Error("render() method must be implemented by subclass");
|
|
282
|
-
}
|
|
283
|
-
} as T;
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* 工具函数:创建响应式组件实例
|
|
289
|
-
*/
|
|
290
|
-
export function createReactiveComponent<T extends WebComponent>(
|
|
291
|
-
ComponentClass: new (...args: any[]) => T,
|
|
292
|
-
config?: ReactiveWebComponentConfig
|
|
293
|
-
): T {
|
|
294
|
-
// 如果已经是响应式组件,直接创建实例
|
|
295
|
-
if (ComponentClass.prototype instanceof ReactiveWebComponent) {
|
|
296
|
-
return new ComponentClass(config);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// 否则使用装饰器包装
|
|
300
|
-
const ReactiveComponent = makeReactive(config?.debug)(ComponentClass);
|
|
301
|
-
return new ReactiveComponent(config);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// 导出响应式相关的所有功能
|
|
305
|
-
export { reactive, createState, ReactiveDebug } from "./utils/reactive";
|
|
306
|
-
export type { ReactiveCallback } from "./utils/reactive";
|