@wsxjs/wsx-core 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-CZII6RG2.mjs +229 -0
- package/dist/index.js +451 -140
- package/dist/index.mjs +445 -141
- package/dist/jsx-runtime.js +7 -0
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +7 -0
- package/dist/jsx.mjs +1 -1
- package/package.json +1 -1
- package/src/base-component.ts +329 -2
- package/src/jsx-factory.ts +15 -0
- package/src/light-component.ts +89 -23
- package/src/reactive-decorator.ts +33 -6
- package/src/utils/reactive.ts +209 -35
- package/src/web-component.ts +67 -154
package/src/utils/reactive.ts
CHANGED
|
@@ -49,7 +49,7 @@ class UpdateScheduler {
|
|
|
49
49
|
try {
|
|
50
50
|
callback();
|
|
51
51
|
} catch (error) {
|
|
52
|
-
|
|
52
|
+
logger.error("[WSX Reactive] Error in callback:", error);
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
55
|
}
|
|
@@ -58,6 +58,82 @@ class UpdateScheduler {
|
|
|
58
58
|
// 全局调度器实例
|
|
59
59
|
const scheduler = new UpdateScheduler();
|
|
60
60
|
|
|
61
|
+
// Proxy 缓存:避免为同一个对象创建多个 Proxy
|
|
62
|
+
// 使用 WeakMap 确保对象被垃圾回收时,对应的 Proxy 也被清理
|
|
63
|
+
// Key: 原始对象, Value: Proxy
|
|
64
|
+
const proxyCache = new WeakMap<object, unknown>();
|
|
65
|
+
|
|
66
|
+
// 反向映射:从 Proxy 到原始对象
|
|
67
|
+
// 用于在 set trap 中比较原始对象而不是 Proxy
|
|
68
|
+
const originalCache = new WeakMap<object, object>();
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 递归展开 Proxy,返回完全干净的对象(不包含任何 Proxy)
|
|
72
|
+
* 用于 JSON.stringify 等需要序列化的场景
|
|
73
|
+
* 使用 WeakSet 防止循环引用导致的无限递归
|
|
74
|
+
*/
|
|
75
|
+
const unwrappingSet = new WeakSet<object>();
|
|
76
|
+
|
|
77
|
+
function unwrapProxy(value: unknown): unknown {
|
|
78
|
+
if (value == null || typeof value !== "object") {
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 如果是 Proxy,获取原始对象
|
|
83
|
+
let original = value;
|
|
84
|
+
if (originalCache.has(value)) {
|
|
85
|
+
original = originalCache.get(value)!;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 防止循环引用
|
|
89
|
+
if (unwrappingSet.has(original)) {
|
|
90
|
+
return null; // 循环引用时返回 null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
unwrappingSet.add(original);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
if (Array.isArray(original)) {
|
|
97
|
+
return original.map((item) => unwrapProxy(item));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const result: Record<string, unknown> = {};
|
|
101
|
+
// 直接访问原始对象的属性,不通过 Proxy
|
|
102
|
+
for (const key in original) {
|
|
103
|
+
if (Object.prototype.hasOwnProperty.call(original, key)) {
|
|
104
|
+
const propValue = original[key];
|
|
105
|
+
// 如果属性值是 Proxy,先获取原始对象再递归
|
|
106
|
+
if (
|
|
107
|
+
propValue != null &&
|
|
108
|
+
typeof propValue === "object" &&
|
|
109
|
+
originalCache.has(propValue)
|
|
110
|
+
) {
|
|
111
|
+
result[key] = unwrapProxy(originalCache.get(propValue)!);
|
|
112
|
+
} else {
|
|
113
|
+
result[key] = unwrapProxy(propValue);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result;
|
|
119
|
+
} finally {
|
|
120
|
+
unwrappingSet.delete(original);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 数组变异方法列表 - 这些方法会修改数组内容
|
|
126
|
+
*/
|
|
127
|
+
const ARRAY_MUTATION_METHODS = [
|
|
128
|
+
"push",
|
|
129
|
+
"pop",
|
|
130
|
+
"shift",
|
|
131
|
+
"unshift",
|
|
132
|
+
"splice",
|
|
133
|
+
"sort",
|
|
134
|
+
"reverse",
|
|
135
|
+
] as const;
|
|
136
|
+
|
|
61
137
|
/**
|
|
62
138
|
* 创建响应式对象
|
|
63
139
|
*
|
|
@@ -66,13 +142,34 @@ const scheduler = new UpdateScheduler();
|
|
|
66
142
|
* @returns 响应式代理对象
|
|
67
143
|
*/
|
|
68
144
|
export function reactive<T extends object>(obj: T, onChange: ReactiveCallback): T {
|
|
69
|
-
|
|
70
|
-
|
|
145
|
+
// 检查缓存,避免为同一个对象创建多个 Proxy
|
|
146
|
+
if (proxyCache.has(obj)) {
|
|
147
|
+
return proxyCache.get(obj) as T;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 检查是否为数组
|
|
151
|
+
const isArray = Array.isArray(obj);
|
|
152
|
+
|
|
153
|
+
const proxy = new Proxy(obj, {
|
|
154
|
+
set(target: T, key: string | symbol, value: unknown): boolean {
|
|
71
155
|
const oldValue = target[key as keyof T];
|
|
72
156
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
157
|
+
// 获取原始对象进行比较(如果 oldValue 是 Proxy)
|
|
158
|
+
const oldOriginal = originalCache.get(oldValue as object) || oldValue;
|
|
159
|
+
const newOriginal =
|
|
160
|
+
value != null && typeof value === "object"
|
|
161
|
+
? originalCache.get(value as object) || value
|
|
162
|
+
: value;
|
|
163
|
+
|
|
164
|
+
// 只有值真正改变时才触发更新(比较原始对象)
|
|
165
|
+
if (oldOriginal !== newOriginal) {
|
|
166
|
+
// 如果新值是对象或数组,确保它也被包装为响应式
|
|
167
|
+
if (value != null && typeof value === "object") {
|
|
168
|
+
const reactiveValue = reactive(value as object, onChange);
|
|
169
|
+
target[key as keyof T] = reactiveValue as T[keyof T];
|
|
170
|
+
} else {
|
|
171
|
+
target[key as keyof T] = value as T[keyof T];
|
|
172
|
+
}
|
|
76
173
|
|
|
77
174
|
// 调度更新
|
|
78
175
|
scheduler.schedule(onChange);
|
|
@@ -81,22 +178,62 @@ export function reactive<T extends object>(obj: T, onChange: ReactiveCallback):
|
|
|
81
178
|
return true;
|
|
82
179
|
},
|
|
83
180
|
|
|
84
|
-
get(target: T, key: string | symbol):
|
|
85
|
-
|
|
181
|
+
get(target: T, key: string | symbol): unknown {
|
|
182
|
+
// 支持 toJSON,让 JSON.stringify 使用原始对象,避免触发 Proxy trap 递归
|
|
183
|
+
if (key === "toJSON") {
|
|
184
|
+
return function () {
|
|
185
|
+
// 递归展开所有嵌套 Proxy,返回完全干净的对象
|
|
186
|
+
return unwrapProxy(obj);
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const value = target[key as keyof T];
|
|
191
|
+
|
|
192
|
+
// 如果是数组,拦截数组变异方法
|
|
193
|
+
if (
|
|
194
|
+
isArray &&
|
|
195
|
+
typeof key === "string" &&
|
|
196
|
+
ARRAY_MUTATION_METHODS.includes(key as (typeof ARRAY_MUTATION_METHODS)[number])
|
|
197
|
+
) {
|
|
198
|
+
return function (this: unknown, ...args: unknown[]) {
|
|
199
|
+
// 调用原始方法
|
|
200
|
+
const arrayMethod = Array.prototype[key as keyof Array<unknown>] as (
|
|
201
|
+
...args: unknown[]
|
|
202
|
+
) => unknown;
|
|
203
|
+
const result = arrayMethod.apply(target, args);
|
|
204
|
+
|
|
205
|
+
// 数组内容已改变,触发更新
|
|
206
|
+
scheduler.schedule(onChange);
|
|
207
|
+
|
|
208
|
+
return result;
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 如果值是对象或数组,自动包装为响应式(支持嵌套对象)
|
|
213
|
+
if (value != null && typeof value === "object") {
|
|
214
|
+
// 先检查缓存,避免重复创建 Proxy
|
|
215
|
+
// 如果对象已经在缓存中,直接返回缓存的 Proxy(使用相同的 onChange)
|
|
216
|
+
if (proxyCache.has(value)) {
|
|
217
|
+
return proxyCache.get(value);
|
|
218
|
+
}
|
|
219
|
+
// 否则创建新的 Proxy
|
|
220
|
+
return reactive(value, onChange);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return value;
|
|
86
224
|
},
|
|
87
225
|
|
|
88
226
|
has(target: T, key: string | symbol): boolean {
|
|
89
227
|
return key in target;
|
|
90
228
|
},
|
|
229
|
+
});
|
|
91
230
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
231
|
+
// 缓存 Proxy,避免重复创建
|
|
232
|
+
proxyCache.set(obj, proxy);
|
|
233
|
+
// 缓存反向映射:从 Proxy 到原始对象
|
|
234
|
+
originalCache.set(proxy, obj);
|
|
95
235
|
|
|
96
|
-
|
|
97
|
-
return Reflect.getOwnPropertyDescriptor(target, key);
|
|
98
|
-
},
|
|
99
|
-
});
|
|
236
|
+
return proxy;
|
|
100
237
|
}
|
|
101
238
|
|
|
102
239
|
/**
|
|
@@ -128,18 +265,6 @@ export function createState<T>(
|
|
|
128
265
|
return [getter, setter];
|
|
129
266
|
}
|
|
130
267
|
|
|
131
|
-
/**
|
|
132
|
-
* 检查一个值是否为响应式对象
|
|
133
|
-
*/
|
|
134
|
-
export function isReactive(value: any): boolean {
|
|
135
|
-
return (
|
|
136
|
-
value != null &&
|
|
137
|
-
typeof value === "object" &&
|
|
138
|
-
value.constructor === Object &&
|
|
139
|
-
typeof value.valueOf === "function"
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
268
|
/**
|
|
144
269
|
* 开发模式下的调试工具
|
|
145
270
|
*/
|
|
@@ -149,7 +274,7 @@ export const ReactiveDebug = {
|
|
|
149
274
|
*/
|
|
150
275
|
enable(): void {
|
|
151
276
|
if (typeof window !== "undefined") {
|
|
152
|
-
(window as
|
|
277
|
+
(window as Window & { __WSX_REACTIVE_DEBUG__?: boolean }).__WSX_REACTIVE_DEBUG__ = true;
|
|
153
278
|
}
|
|
154
279
|
},
|
|
155
280
|
|
|
@@ -158,7 +283,8 @@ export const ReactiveDebug = {
|
|
|
158
283
|
*/
|
|
159
284
|
disable(): void {
|
|
160
285
|
if (typeof window !== "undefined") {
|
|
161
|
-
(window as
|
|
286
|
+
(window as Window & { __WSX_REACTIVE_DEBUG__?: boolean }).__WSX_REACTIVE_DEBUG__ =
|
|
287
|
+
false;
|
|
162
288
|
}
|
|
163
289
|
},
|
|
164
290
|
|
|
@@ -166,13 +292,17 @@ export const ReactiveDebug = {
|
|
|
166
292
|
* 检查是否启用调试模式
|
|
167
293
|
*/
|
|
168
294
|
isEnabled(): boolean {
|
|
169
|
-
return
|
|
295
|
+
return (
|
|
296
|
+
typeof window !== "undefined" &&
|
|
297
|
+
(window as Window & { __WSX_REACTIVE_DEBUG__?: boolean }).__WSX_REACTIVE_DEBUG__ ===
|
|
298
|
+
true
|
|
299
|
+
);
|
|
170
300
|
},
|
|
171
301
|
|
|
172
302
|
/**
|
|
173
303
|
* 调试日志
|
|
174
304
|
*/
|
|
175
|
-
log(message: string, ...args:
|
|
305
|
+
log(message: string, ...args: unknown[]): void {
|
|
176
306
|
if (this.isEnabled()) {
|
|
177
307
|
logger.info(`[WSX Reactive] ${message}`, ...args);
|
|
178
308
|
}
|
|
@@ -188,9 +318,10 @@ export function reactiveWithDebug<T extends object>(
|
|
|
188
318
|
debugName?: string
|
|
189
319
|
): T {
|
|
190
320
|
const name = debugName || obj.constructor.name || "Unknown";
|
|
321
|
+
const isArray = Array.isArray(obj);
|
|
191
322
|
|
|
192
323
|
return new Proxy(obj, {
|
|
193
|
-
set(target: T, key: string | symbol, value:
|
|
324
|
+
set(target: T, key: string | symbol, value: unknown): boolean {
|
|
194
325
|
const oldValue = target[key as keyof T];
|
|
195
326
|
|
|
196
327
|
if (oldValue !== value) {
|
|
@@ -200,15 +331,58 @@ export function reactiveWithDebug<T extends object>(
|
|
|
200
331
|
newValue: value,
|
|
201
332
|
});
|
|
202
333
|
|
|
203
|
-
target[key as keyof T] = value;
|
|
334
|
+
target[key as keyof T] = value as T[keyof T];
|
|
204
335
|
scheduler.schedule(onChange);
|
|
205
336
|
}
|
|
206
337
|
|
|
207
338
|
return true;
|
|
208
339
|
},
|
|
209
340
|
|
|
210
|
-
get(target: T, key: string | symbol):
|
|
211
|
-
|
|
341
|
+
get(target: T, key: string | symbol): unknown {
|
|
342
|
+
// 支持 toJSON,让 JSON.stringify 使用原始对象,避免触发 Proxy trap 递归
|
|
343
|
+
if (key === "toJSON") {
|
|
344
|
+
return function () {
|
|
345
|
+
// 递归展开所有嵌套 Proxy,返回完全干净的对象
|
|
346
|
+
return unwrapProxy(obj);
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const value = target[key as keyof T];
|
|
351
|
+
|
|
352
|
+
// 如果是数组,拦截数组变异方法
|
|
353
|
+
if (
|
|
354
|
+
isArray &&
|
|
355
|
+
typeof key === "string" &&
|
|
356
|
+
ARRAY_MUTATION_METHODS.includes(key as (typeof ARRAY_MUTATION_METHODS)[number])
|
|
357
|
+
) {
|
|
358
|
+
return function (this: unknown, ...args: unknown[]) {
|
|
359
|
+
ReactiveDebug.log(`Array mutation in ${name}:`, {
|
|
360
|
+
method: key,
|
|
361
|
+
args,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// 调用原始方法
|
|
365
|
+
const arrayMethod = Array.prototype[key as keyof Array<unknown>] as (
|
|
366
|
+
...args: unknown[]
|
|
367
|
+
) => unknown;
|
|
368
|
+
const result = arrayMethod.apply(target, args);
|
|
369
|
+
|
|
370
|
+
// 数组内容已改变,触发更新
|
|
371
|
+
scheduler.schedule(onChange);
|
|
372
|
+
|
|
373
|
+
return result;
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 如果值是对象或数组,自动包装为响应式(支持嵌套对象)
|
|
378
|
+
if (value != null && typeof value === "object") {
|
|
379
|
+
// 检查是否已经是响应式(通过检查是否有 Proxy 标记)
|
|
380
|
+
// 简单检查:如果值已经是 Proxy,直接返回
|
|
381
|
+
// 否则,递归包装为响应式
|
|
382
|
+
return reactiveWithDebug(value, onChange, `${name}.${String(key)}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return value;
|
|
212
386
|
},
|
|
213
387
|
});
|
|
214
388
|
}
|
package/src/web-component.ts
CHANGED
|
@@ -11,24 +11,14 @@
|
|
|
11
11
|
import { h, type JSXChildren } from "./jsx-factory";
|
|
12
12
|
import { StyleManager } from "./styles/style-manager";
|
|
13
13
|
import { BaseComponent, type BaseComponentConfig } from "./base-component";
|
|
14
|
+
import { createLogger } from "./utils/logger";
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
* Web Component 配置接口
|
|
17
|
-
*/
|
|
18
|
-
export interface WebComponentConfig extends BaseComponentConfig {
|
|
19
|
-
preserveFocus?: boolean; // 是否在重渲染时保持焦点
|
|
20
|
-
}
|
|
16
|
+
const logger = createLogger("WebComponent");
|
|
21
17
|
|
|
22
18
|
/**
|
|
23
|
-
*
|
|
19
|
+
* Web Component 配置接口
|
|
24
20
|
*/
|
|
25
|
-
|
|
26
|
-
tagName: string;
|
|
27
|
-
className: string;
|
|
28
|
-
value?: string;
|
|
29
|
-
selectionStart?: number;
|
|
30
|
-
selectionEnd?: number;
|
|
31
|
-
}
|
|
21
|
+
export type WebComponentConfig = BaseComponentConfig;
|
|
32
22
|
|
|
33
23
|
/**
|
|
34
24
|
* 通用 WSX Web Component 基础抽象类
|
|
@@ -36,13 +26,10 @@ interface FocusData {
|
|
|
36
26
|
export abstract class WebComponent extends BaseComponent {
|
|
37
27
|
declare shadowRoot: ShadowRoot;
|
|
38
28
|
protected config!: WebComponentConfig; // Initialized by BaseComponent constructor
|
|
39
|
-
private _preserveFocus: boolean = true;
|
|
40
29
|
|
|
41
30
|
constructor(config: WebComponentConfig = {}) {
|
|
42
31
|
super(config);
|
|
43
32
|
// BaseComponent already created this.config with getter for styles
|
|
44
|
-
// Just update preserveFocus property
|
|
45
|
-
this._preserveFocus = config.preserveFocus ?? true;
|
|
46
33
|
this.attachShadow({ mode: "open" });
|
|
47
34
|
// Styles are applied in connectedCallback for consistency with LightComponent
|
|
48
35
|
// and to ensure all class properties (including getters) are initialized
|
|
@@ -62,9 +49,8 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
62
49
|
this.connected = true;
|
|
63
50
|
try {
|
|
64
51
|
// 应用CSS样式到Shadow DOM
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
const stylesToApply = this._getAutoStyles?.() || this.config.styles;
|
|
52
|
+
// Check both _autoStyles getter and config.styles getter
|
|
53
|
+
const stylesToApply = this._autoStyles || this.config.styles;
|
|
68
54
|
if (stylesToApply) {
|
|
69
55
|
const styleName = this.config.styleName || this.constructor.name;
|
|
70
56
|
StyleManager.applyStyles(this.shadowRoot, styleName, stylesToApply);
|
|
@@ -74,10 +60,13 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
74
60
|
const content = this.render();
|
|
75
61
|
this.shadowRoot.appendChild(content);
|
|
76
62
|
|
|
63
|
+
// 初始化事件监听器
|
|
64
|
+
this.initializeEventListeners();
|
|
65
|
+
|
|
77
66
|
// 调用子类的初始化钩子
|
|
78
67
|
this.onConnected?.();
|
|
79
68
|
} catch (error) {
|
|
80
|
-
|
|
69
|
+
logger.error(`Error in connectedCallback:`, error);
|
|
81
70
|
this.renderError(error);
|
|
82
71
|
}
|
|
83
72
|
}
|
|
@@ -87,6 +76,7 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
87
76
|
*/
|
|
88
77
|
disconnectedCallback(): void {
|
|
89
78
|
this.connected = false;
|
|
79
|
+
this.cleanup(); // 清理资源(包括防抖定时器)
|
|
90
80
|
this.onDisconnected?.();
|
|
91
81
|
}
|
|
92
82
|
|
|
@@ -115,156 +105,79 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
115
105
|
*/
|
|
116
106
|
protected rerender(): void {
|
|
117
107
|
if (!this.connected) {
|
|
118
|
-
|
|
119
|
-
`[${this.constructor.name}] Component is not connected, skipping rerender.`
|
|
120
|
-
);
|
|
108
|
+
logger.warn("Component is not connected, skipping rerender.");
|
|
121
109
|
return;
|
|
122
110
|
}
|
|
123
111
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
focusData = this.saveFocusState(activeElement);
|
|
129
|
-
}
|
|
112
|
+
// 1. 捕获焦点状态(在 DOM 替换之前)
|
|
113
|
+
const focusState = this.captureFocusState();
|
|
114
|
+
// 保存到实例变量,供 render() 使用(如果需要)
|
|
115
|
+
this._pendingFocusState = focusState;
|
|
130
116
|
|
|
131
117
|
// 保存当前的 adopted stylesheets (jsdom may not support this)
|
|
132
118
|
const adoptedStyleSheets = this.shadowRoot.adoptedStyleSheets || [];
|
|
133
119
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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);
|
|
120
|
+
try {
|
|
121
|
+
// 只有在没有 adopted stylesheets 时才重新应用样式
|
|
122
|
+
// Check both _autoStyles getter and config.styles getter
|
|
123
|
+
if (adoptedStyleSheets.length === 0) {
|
|
124
|
+
const stylesToApply = this._autoStyles || this.config.styles;
|
|
125
|
+
if (stylesToApply) {
|
|
126
|
+
const styleName = this.config.styleName || this.constructor.name;
|
|
127
|
+
StyleManager.applyStyles(this.shadowRoot, styleName, stylesToApply);
|
|
128
|
+
}
|
|
149
129
|
}
|
|
150
|
-
}
|
|
151
130
|
|
|
152
|
-
|
|
153
|
-
try {
|
|
131
|
+
// 重新渲染JSX
|
|
154
132
|
const content = this.render();
|
|
155
|
-
this.shadowRoot.appendChild(content);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
console.error(`[${this.constructor.name}] Error in rerender:`, error);
|
|
158
|
-
this.renderError(error);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// 恢复焦点状态
|
|
162
|
-
if (this._preserveFocus && focusData && this.shadowRoot) {
|
|
163
|
-
this.restoreFocusState(focusData);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
133
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (selection && selection.rangeCount > 0) {
|
|
184
|
-
const range = selection.getRangeAt(0);
|
|
185
|
-
focusData.selectionStart = range.startOffset;
|
|
186
|
-
focusData.selectionEnd = range.endOffset;
|
|
134
|
+
// 在添加到 DOM 之前恢复值,避免浏览器渲染状态值
|
|
135
|
+
// 这样可以确保值在元素添加到 DOM 之前就是正确的
|
|
136
|
+
if (focusState && focusState.key && focusState.value !== undefined) {
|
|
137
|
+
// 在 content 树中查找目标元素
|
|
138
|
+
const target = content.querySelector(
|
|
139
|
+
`[data-wsx-key="${focusState.key}"]`
|
|
140
|
+
) as HTMLElement;
|
|
141
|
+
|
|
142
|
+
if (target) {
|
|
143
|
+
if (
|
|
144
|
+
target instanceof HTMLInputElement ||
|
|
145
|
+
target instanceof HTMLTextAreaElement
|
|
146
|
+
) {
|
|
147
|
+
target.value = focusState.value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
187
150
|
}
|
|
188
|
-
}
|
|
189
151
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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;
|
|
152
|
+
// 恢复 adopted stylesheets (避免重新应用样式)
|
|
153
|
+
if (this.shadowRoot.adoptedStyleSheets) {
|
|
154
|
+
this.shadowRoot.adoptedStyleSheets = adoptedStyleSheets;
|
|
199
155
|
}
|
|
200
|
-
}
|
|
201
156
|
|
|
202
|
-
|
|
203
|
-
|
|
157
|
+
// 使用 requestAnimationFrame 批量执行 DOM 操作,减少重绘
|
|
158
|
+
// 在同一帧中完成添加和移除,避免中间状态
|
|
159
|
+
requestAnimationFrame(() => {
|
|
160
|
+
// 先添加新内容
|
|
161
|
+
this.shadowRoot.appendChild(content);
|
|
204
162
|
|
|
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]}`
|
|
163
|
+
// 立即移除旧内容(在同一帧中,浏览器会批量处理)
|
|
164
|
+
const oldChildren = Array.from(this.shadowRoot.children).filter(
|
|
165
|
+
(child) => child !== content
|
|
218
166
|
);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
167
|
+
oldChildren.forEach((child) => child.remove());
|
|
168
|
+
|
|
169
|
+
// 恢复焦点状态(在 DOM 替换之后)
|
|
170
|
+
// 值已经在添加到 DOM 之前恢复了,这里只需要恢复焦点和光标位置
|
|
171
|
+
// 使用另一个 requestAnimationFrame 确保 DOM 已完全更新
|
|
172
|
+
requestAnimationFrame(() => {
|
|
173
|
+
this.restoreFocusState(focusState);
|
|
174
|
+
// 清除待处理的焦点状态
|
|
175
|
+
this._pendingFocusState = null;
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
logger.error("Error in rerender:", error);
|
|
180
|
+
this.renderError(error);
|
|
268
181
|
}
|
|
269
182
|
}
|
|
270
183
|
|