@wsxjs/wsx-core 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-CZII6RG2.mjs +229 -0
- package/dist/index.js +445 -140
- package/dist/index.mjs +439 -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 +312 -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/dist/jsx-runtime.js
CHANGED
|
@@ -167,6 +167,13 @@ function h(tag, props = {}, ...children) {
|
|
|
167
167
|
if (value) {
|
|
168
168
|
element.setAttribute(key, "");
|
|
169
169
|
}
|
|
170
|
+
} else if (key === "value") {
|
|
171
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
|
|
172
|
+
element.value = String(value);
|
|
173
|
+
} else {
|
|
174
|
+
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
175
|
+
element.setAttribute(attributeName, String(value));
|
|
176
|
+
}
|
|
170
177
|
} else {
|
|
171
178
|
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
172
179
|
element.setAttribute(attributeName, String(value));
|
package/dist/jsx-runtime.mjs
CHANGED
package/dist/jsx.js
CHANGED
|
@@ -166,6 +166,13 @@ function h(tag, props = {}, ...children) {
|
|
|
166
166
|
if (value) {
|
|
167
167
|
element.setAttribute(key, "");
|
|
168
168
|
}
|
|
169
|
+
} else if (key === "value") {
|
|
170
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
|
|
171
|
+
element.value = String(value);
|
|
172
|
+
} else {
|
|
173
|
+
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
174
|
+
element.setAttribute(attributeName, String(value));
|
|
175
|
+
}
|
|
169
176
|
} else {
|
|
170
177
|
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
171
178
|
element.setAttribute(attributeName, String(value));
|
package/dist/jsx.mjs
CHANGED
package/package.json
CHANGED
package/src/base-component.ts
CHANGED
|
@@ -18,6 +18,19 @@ interface ReactiveStateStorage {
|
|
|
18
18
|
setter: (value: unknown | ((prev: unknown) => unknown)) => void;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Focus state interface for focus preservation during rerender
|
|
23
|
+
*/
|
|
24
|
+
interface FocusState {
|
|
25
|
+
key: string;
|
|
26
|
+
elementType: "input" | "textarea" | "select" | "contenteditable";
|
|
27
|
+
value?: string;
|
|
28
|
+
selectionStart?: number;
|
|
29
|
+
selectionEnd?: number;
|
|
30
|
+
scrollTop?: number; // For textarea
|
|
31
|
+
selectedIndex?: number; // For select
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
/**
|
|
22
35
|
* Base configuration interface
|
|
23
36
|
*/
|
|
@@ -47,6 +60,24 @@ export abstract class BaseComponent extends HTMLElement {
|
|
|
47
60
|
*/
|
|
48
61
|
protected _autoStyles?: string;
|
|
49
62
|
|
|
63
|
+
/**
|
|
64
|
+
* 当前捕获的焦点状态(用于在 render 时使用捕获的值)
|
|
65
|
+
* @internal - 由 rerender() 方法管理
|
|
66
|
+
*/
|
|
67
|
+
protected _pendingFocusState: FocusState | null = null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 防抖定时器,用于延迟重渲染(当用户正在输入时)
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
private _rerenderDebounceTimer: number | null = null;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 待处理的重渲染标志(当用户正在输入时,标记需要重渲染但延迟执行)
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
private _pendingRerender: boolean = false;
|
|
80
|
+
|
|
50
81
|
/**
|
|
51
82
|
* 子类应该重写这个方法来定义观察的属性
|
|
52
83
|
* @returns 要观察的属性名数组
|
|
@@ -95,6 +126,35 @@ export abstract class BaseComponent extends HTMLElement {
|
|
|
95
126
|
*/
|
|
96
127
|
protected onConnected?(): void;
|
|
97
128
|
|
|
129
|
+
/**
|
|
130
|
+
* 处理 blur 事件,在用户停止输入时执行待处理的重渲染
|
|
131
|
+
* @internal
|
|
132
|
+
*/
|
|
133
|
+
private handleGlobalBlur = (event: FocusEvent): void => {
|
|
134
|
+
// 检查 blur 的元素是否在组件内
|
|
135
|
+
const root = this.getActiveRoot();
|
|
136
|
+
const target = event.target as HTMLElement;
|
|
137
|
+
|
|
138
|
+
if (target && root.contains(target)) {
|
|
139
|
+
// 用户停止输入,执行待处理的重渲染
|
|
140
|
+
if (this._pendingRerender && this.connected) {
|
|
141
|
+
// 清除防抖定时器
|
|
142
|
+
if (this._rerenderDebounceTimer !== null) {
|
|
143
|
+
clearTimeout(this._rerenderDebounceTimer);
|
|
144
|
+
this._rerenderDebounceTimer = null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 延迟一小段时间后重渲染,确保 blur 事件完全处理
|
|
148
|
+
requestAnimationFrame(() => {
|
|
149
|
+
if (this._pendingRerender && this.connected) {
|
|
150
|
+
this._pendingRerender = false;
|
|
151
|
+
this.rerender();
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
98
158
|
/**
|
|
99
159
|
* 可选生命周期钩子:组件已断开
|
|
100
160
|
*/
|
|
@@ -157,11 +217,82 @@ export abstract class BaseComponent extends HTMLElement {
|
|
|
157
217
|
/**
|
|
158
218
|
* 调度重渲染
|
|
159
219
|
* 这个方法被响应式系统调用,开发者通常不需要直接调用
|
|
220
|
+
* 使用 queueMicrotask 进行异步调度,与 reactive() 系统保持一致
|
|
160
221
|
*/
|
|
161
222
|
protected scheduleRerender(): void {
|
|
162
|
-
if (this.connected) {
|
|
163
|
-
|
|
223
|
+
if (!this.connected) {
|
|
224
|
+
// 如果组件已断开,清除定时器
|
|
225
|
+
if (this._rerenderDebounceTimer !== null) {
|
|
226
|
+
clearTimeout(this._rerenderDebounceTimer);
|
|
227
|
+
this._rerenderDebounceTimer = null;
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 检查是否有焦点元素(用户可能正在输入)
|
|
233
|
+
const root = this.getActiveRoot();
|
|
234
|
+
let hasActiveElement = false;
|
|
235
|
+
|
|
236
|
+
if (root instanceof ShadowRoot) {
|
|
237
|
+
hasActiveElement = root.activeElement !== null;
|
|
238
|
+
} else {
|
|
239
|
+
const docActiveElement = document.activeElement;
|
|
240
|
+
hasActiveElement = docActiveElement !== null && root.contains(docActiveElement);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 如果用户正在输入,完全跳过重渲染,只在 blur 时更新
|
|
244
|
+
// 这样可以完全避免输入时的闪烁
|
|
245
|
+
if (hasActiveElement) {
|
|
246
|
+
// 标记需要重渲染,但延迟到 blur 事件
|
|
247
|
+
this._pendingRerender = true;
|
|
248
|
+
|
|
249
|
+
// 清除之前的定时器(不再使用定时器,只等待 blur)
|
|
250
|
+
if (this._rerenderDebounceTimer !== null) {
|
|
251
|
+
clearTimeout(this._rerenderDebounceTimer);
|
|
252
|
+
this._rerenderDebounceTimer = null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 不执行重渲染,等待 blur 事件
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 没有焦点元素,立即重渲染(使用 queueMicrotask 批量处理)
|
|
260
|
+
// 如果有待处理的重渲染,也立即执行
|
|
261
|
+
if (this._pendingRerender) {
|
|
262
|
+
this._pendingRerender = false;
|
|
263
|
+
}
|
|
264
|
+
queueMicrotask(() => {
|
|
265
|
+
if (this.connected) {
|
|
266
|
+
this.rerender();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 清理资源(在组件断开连接时调用)
|
|
273
|
+
* @internal
|
|
274
|
+
*/
|
|
275
|
+
protected cleanup(): void {
|
|
276
|
+
// 清除防抖定时器
|
|
277
|
+
if (this._rerenderDebounceTimer !== null) {
|
|
278
|
+
clearTimeout(this._rerenderDebounceTimer);
|
|
279
|
+
this._rerenderDebounceTimer = null;
|
|
164
280
|
}
|
|
281
|
+
|
|
282
|
+
// 移除 blur 事件监听器
|
|
283
|
+
document.removeEventListener("blur", this.handleGlobalBlur, true);
|
|
284
|
+
|
|
285
|
+
// 清除待处理的重渲染标志
|
|
286
|
+
this._pendingRerender = false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 初始化事件监听器(在组件连接时调用)
|
|
291
|
+
* @internal
|
|
292
|
+
*/
|
|
293
|
+
protected initializeEventListeners(): void {
|
|
294
|
+
// 添加 blur 事件监听器,在用户停止输入时执行待处理的重渲染
|
|
295
|
+
document.addEventListener("blur", this.handleGlobalBlur, true);
|
|
165
296
|
}
|
|
166
297
|
|
|
167
298
|
/**
|
|
@@ -236,4 +367,183 @@ export abstract class BaseComponent extends HTMLElement {
|
|
|
236
367
|
protected cleanupReactiveStates(): void {
|
|
237
368
|
this._reactiveStates.clear();
|
|
238
369
|
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 获取当前活动的 DOM 根(Shadow DOM 或 Light DOM)
|
|
373
|
+
* @returns 活动的 DOM 根元素
|
|
374
|
+
*/
|
|
375
|
+
protected getActiveRoot(): ShadowRoot | HTMLElement {
|
|
376
|
+
// WebComponent 使用 shadowRoot,LightComponent 使用自身
|
|
377
|
+
if ("shadowRoot" in this && this.shadowRoot) {
|
|
378
|
+
return this.shadowRoot;
|
|
379
|
+
}
|
|
380
|
+
return this;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* 捕获当前焦点状态(在重渲染之前调用)
|
|
385
|
+
* @returns 焦点状态,如果没有焦点元素则返回 null
|
|
386
|
+
*/
|
|
387
|
+
protected captureFocusState(): FocusState | null {
|
|
388
|
+
const root = this.getActiveRoot();
|
|
389
|
+
let activeElement: Element | null = null;
|
|
390
|
+
|
|
391
|
+
// 获取活动元素
|
|
392
|
+
if (root instanceof ShadowRoot) {
|
|
393
|
+
// Shadow DOM: 使用 shadowRoot.activeElement
|
|
394
|
+
activeElement = root.activeElement;
|
|
395
|
+
} else {
|
|
396
|
+
// Light DOM: 检查 document.activeElement 是否在组件内
|
|
397
|
+
const docActiveElement = document.activeElement;
|
|
398
|
+
if (docActiveElement && root.contains(docActiveElement)) {
|
|
399
|
+
activeElement = docActiveElement;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (!activeElement || !(activeElement instanceof HTMLElement)) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 检查元素是否有 data-wsx-key 属性
|
|
408
|
+
const key = activeElement.getAttribute("data-wsx-key");
|
|
409
|
+
if (!key) {
|
|
410
|
+
return null; // 元素没有 key,跳过焦点保持
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const tagName = activeElement.tagName.toLowerCase();
|
|
414
|
+
const state: FocusState = {
|
|
415
|
+
key,
|
|
416
|
+
elementType: tagName as FocusState["elementType"],
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// 处理 input 和 textarea
|
|
420
|
+
if (
|
|
421
|
+
activeElement instanceof HTMLInputElement ||
|
|
422
|
+
activeElement instanceof HTMLTextAreaElement
|
|
423
|
+
) {
|
|
424
|
+
state.value = activeElement.value;
|
|
425
|
+
state.selectionStart = activeElement.selectionStart ?? undefined;
|
|
426
|
+
state.selectionEnd = activeElement.selectionEnd ?? undefined;
|
|
427
|
+
|
|
428
|
+
// 对于 textarea,保存滚动位置
|
|
429
|
+
if (activeElement instanceof HTMLTextAreaElement) {
|
|
430
|
+
state.scrollTop = activeElement.scrollTop;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// 处理 select
|
|
434
|
+
else if (activeElement instanceof HTMLSelectElement) {
|
|
435
|
+
state.elementType = "select";
|
|
436
|
+
state.selectedIndex = activeElement.selectedIndex;
|
|
437
|
+
}
|
|
438
|
+
// 处理 contenteditable
|
|
439
|
+
else if (activeElement.hasAttribute("contenteditable")) {
|
|
440
|
+
state.elementType = "contenteditable";
|
|
441
|
+
const selection = window.getSelection();
|
|
442
|
+
if (selection && selection.rangeCount > 0) {
|
|
443
|
+
const range = selection.getRangeAt(0);
|
|
444
|
+
state.selectionStart = range.startOffset;
|
|
445
|
+
state.selectionEnd = range.endOffset;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return state;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* 恢复焦点状态(在重渲染之后调用)
|
|
454
|
+
* @param state - 之前捕获的焦点状态
|
|
455
|
+
*/
|
|
456
|
+
protected restoreFocusState(state: FocusState | null): void {
|
|
457
|
+
if (!state || !state.key) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const root = this.getActiveRoot();
|
|
462
|
+
const target = root.querySelector(`[data-wsx-key="${state.key}"]`) as HTMLElement;
|
|
463
|
+
|
|
464
|
+
if (!target) {
|
|
465
|
+
return; // 元素未找到,跳过恢复
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 立即同步恢复值,避免闪烁
|
|
469
|
+
// 这必须在 appendChild 之后立即执行,在浏览器渲染之前
|
|
470
|
+
if (state.value !== undefined) {
|
|
471
|
+
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
|
|
472
|
+
// 直接设置 value 属性,覆盖 render() 中设置的值
|
|
473
|
+
// 使用 .value 而不是 setAttribute,因为 .value 是当前值,setAttribute 是初始值
|
|
474
|
+
target.value = state.value;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 恢复 select 状态
|
|
479
|
+
if (state.selectedIndex !== undefined && target instanceof HTMLSelectElement) {
|
|
480
|
+
target.selectedIndex = state.selectedIndex;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// 使用 requestAnimationFrame 恢复焦点和光标位置
|
|
484
|
+
// 这样可以确保 DOM 完全更新,但值已经同步恢复了
|
|
485
|
+
requestAnimationFrame(() => {
|
|
486
|
+
// 再次查找元素(确保元素仍然存在)
|
|
487
|
+
const currentTarget = root.querySelector(
|
|
488
|
+
`[data-wsx-key="${state.key}"]`
|
|
489
|
+
) as HTMLElement;
|
|
490
|
+
|
|
491
|
+
if (!currentTarget) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 再次确保值正确(防止被其他代码覆盖)
|
|
496
|
+
if (state.value !== undefined) {
|
|
497
|
+
if (
|
|
498
|
+
currentTarget instanceof HTMLInputElement ||
|
|
499
|
+
currentTarget instanceof HTMLTextAreaElement
|
|
500
|
+
) {
|
|
501
|
+
// 只有在值不同时才更新,避免触发额外的事件
|
|
502
|
+
if (currentTarget.value !== state.value) {
|
|
503
|
+
currentTarget.value = state.value;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 聚焦元素(防止页面滚动)
|
|
509
|
+
currentTarget.focus({ preventScroll: true });
|
|
510
|
+
|
|
511
|
+
// 恢复光标/选择位置
|
|
512
|
+
if (state.selectionStart !== undefined) {
|
|
513
|
+
if (
|
|
514
|
+
currentTarget instanceof HTMLInputElement ||
|
|
515
|
+
currentTarget instanceof HTMLTextAreaElement
|
|
516
|
+
) {
|
|
517
|
+
const start = state.selectionStart;
|
|
518
|
+
const end = state.selectionEnd ?? start;
|
|
519
|
+
currentTarget.setSelectionRange(start, end);
|
|
520
|
+
|
|
521
|
+
// 恢复 textarea 滚动位置
|
|
522
|
+
if (
|
|
523
|
+
state.scrollTop !== undefined &&
|
|
524
|
+
currentTarget instanceof HTMLTextAreaElement
|
|
525
|
+
) {
|
|
526
|
+
currentTarget.scrollTop = state.scrollTop;
|
|
527
|
+
}
|
|
528
|
+
} else if (currentTarget.hasAttribute("contenteditable")) {
|
|
529
|
+
// 恢复 contenteditable 选择
|
|
530
|
+
const selection = window.getSelection();
|
|
531
|
+
if (selection) {
|
|
532
|
+
const range = document.createRange();
|
|
533
|
+
const textNode = currentTarget.childNodes[0];
|
|
534
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
535
|
+
const maxPos = Math.min(
|
|
536
|
+
state.selectionStart,
|
|
537
|
+
textNode.textContent?.length || 0
|
|
538
|
+
);
|
|
539
|
+
range.setStart(textNode, maxPos);
|
|
540
|
+
range.setEnd(textNode, state.selectionEnd ?? maxPos);
|
|
541
|
+
selection.removeAllRanges();
|
|
542
|
+
selection.addRange(range);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}
|
|
239
549
|
}
|
package/src/jsx-factory.ts
CHANGED
|
@@ -89,6 +89,21 @@ export function h(
|
|
|
89
89
|
element.setAttribute(key, "");
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
// 特殊处理 input/textarea/select 的 value 属性
|
|
93
|
+
// 使用 .value 而不是 setAttribute,因为 .value 是当前值,setAttribute 是初始值
|
|
94
|
+
else if (key === "value") {
|
|
95
|
+
if (
|
|
96
|
+
element instanceof HTMLInputElement ||
|
|
97
|
+
element instanceof HTMLTextAreaElement ||
|
|
98
|
+
element instanceof HTMLSelectElement
|
|
99
|
+
) {
|
|
100
|
+
element.value = String(value);
|
|
101
|
+
} else {
|
|
102
|
+
// 对于其他元素,使用 setAttribute
|
|
103
|
+
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
104
|
+
element.setAttribute(attributeName, String(value));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
92
107
|
// 处理其他属性
|
|
93
108
|
else {
|
|
94
109
|
// 对SVG元素使用正确的属性名
|
package/src/light-component.ts
CHANGED
|
@@ -57,6 +57,9 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
57
57
|
const content = this.render();
|
|
58
58
|
this.appendChild(content);
|
|
59
59
|
|
|
60
|
+
// 初始化事件监听器
|
|
61
|
+
this.initializeEventListeners();
|
|
62
|
+
|
|
60
63
|
// 调用子类的初始化钩子
|
|
61
64
|
this.onConnected?.();
|
|
62
65
|
} catch (error) {
|
|
@@ -69,6 +72,8 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
69
72
|
* Web Component生命周期:从DOM断开
|
|
70
73
|
*/
|
|
71
74
|
disconnectedCallback(): void {
|
|
75
|
+
this.connected = false;
|
|
76
|
+
this.cleanup(); // 清理资源(包括防抖定时器)
|
|
72
77
|
this.cleanupReactiveStates();
|
|
73
78
|
this.cleanupStyles();
|
|
74
79
|
this.onDisconnected?.();
|
|
@@ -105,36 +110,97 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
105
110
|
return;
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
//
|
|
109
|
-
|
|
113
|
+
// 1. 捕获焦点状态(在 DOM 替换之前)
|
|
114
|
+
const focusState = this.captureFocusState();
|
|
115
|
+
// 保存到实例变量,供 render() 使用(如果需要)
|
|
116
|
+
this._pendingFocusState = focusState;
|
|
110
117
|
|
|
111
|
-
// 重新应用样式(必须在内容之前添加,确保样式优先)
|
|
112
|
-
if (this.config.styles) {
|
|
113
|
-
const styleName = this.config.styleName || this.constructor.name;
|
|
114
|
-
// 直接创建并添加样式元素,不检查是否存在(因为 innerHTML = "" 已经清空了)
|
|
115
|
-
const styleElement = document.createElement("style");
|
|
116
|
-
styleElement.setAttribute("data-wsx-light-component", styleName);
|
|
117
|
-
styleElement.textContent = this.config.styles;
|
|
118
|
-
// 使用 prepend 或 insertBefore 确保样式在第一个位置
|
|
119
|
-
// 由于 innerHTML = "" 后 firstChild 是 null,使用 appendChild 然后调整顺序
|
|
120
|
-
this.appendChild(styleElement);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 重新渲染JSX内容
|
|
124
118
|
try {
|
|
119
|
+
// 重新渲染JSX内容
|
|
125
120
|
const content = this.render();
|
|
126
|
-
this.appendChild(content);
|
|
127
121
|
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
122
|
+
// 在添加到 DOM 之前恢复值,避免浏览器渲染状态值
|
|
123
|
+
// 这样可以确保值在元素添加到 DOM 之前就是正确的
|
|
124
|
+
if (focusState && focusState.key && focusState.value !== undefined) {
|
|
125
|
+
// 在 content 树中查找目标元素
|
|
126
|
+
const target = content.querySelector(
|
|
127
|
+
`[data-wsx-key="${focusState.key}"]`
|
|
128
|
+
) as HTMLElement;
|
|
129
|
+
|
|
130
|
+
if (target) {
|
|
131
|
+
if (
|
|
132
|
+
target instanceof HTMLInputElement ||
|
|
133
|
+
target instanceof HTMLTextAreaElement
|
|
134
|
+
) {
|
|
135
|
+
target.value = focusState.value;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 确保样式元素存在
|
|
141
|
+
const stylesToApply = this._autoStyles || this.config.styles;
|
|
142
|
+
if (stylesToApply) {
|
|
143
|
+
const styleName = this.config.styleName || this.constructor.name;
|
|
144
|
+
let styleElement = this.querySelector(
|
|
145
|
+
`style[data-wsx-light-component="${styleName}"]`
|
|
146
|
+
) as HTMLStyleElement;
|
|
147
|
+
|
|
148
|
+
if (!styleElement) {
|
|
149
|
+
// 创建样式元素
|
|
150
|
+
styleElement = document.createElement("style");
|
|
151
|
+
styleElement.setAttribute("data-wsx-light-component", styleName);
|
|
152
|
+
styleElement.textContent = stylesToApply;
|
|
135
153
|
this.insertBefore(styleElement, this.firstChild);
|
|
154
|
+
} else if (styleElement.textContent !== stylesToApply) {
|
|
155
|
+
// 更新样式内容
|
|
156
|
+
styleElement.textContent = stylesToApply;
|
|
136
157
|
}
|
|
137
158
|
}
|
|
159
|
+
|
|
160
|
+
// 使用 requestAnimationFrame 批量执行 DOM 操作,减少重绘
|
|
161
|
+
// 在同一帧中完成添加和移除,避免中间状态
|
|
162
|
+
requestAnimationFrame(() => {
|
|
163
|
+
// 先添加新内容
|
|
164
|
+
this.appendChild(content);
|
|
165
|
+
|
|
166
|
+
// 立即移除旧内容(在同一帧中,浏览器会批量处理)
|
|
167
|
+
const oldChildren = Array.from(this.children).filter((child) => {
|
|
168
|
+
// 保留新添加的内容
|
|
169
|
+
if (child === content) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
// 保留样式元素(如果存在)
|
|
173
|
+
if (
|
|
174
|
+
stylesToApply &&
|
|
175
|
+
child instanceof HTMLStyleElement &&
|
|
176
|
+
child.getAttribute("data-wsx-light-component") ===
|
|
177
|
+
(this.config.styleName || this.constructor.name)
|
|
178
|
+
) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
return true;
|
|
182
|
+
});
|
|
183
|
+
oldChildren.forEach((child) => child.remove());
|
|
184
|
+
|
|
185
|
+
// 确保样式元素在第一个位置
|
|
186
|
+
if (stylesToApply && this.children.length > 1) {
|
|
187
|
+
const styleElement = this.querySelector(
|
|
188
|
+
`style[data-wsx-light-component="${this.config.styleName || this.constructor.name}"]`
|
|
189
|
+
);
|
|
190
|
+
if (styleElement && styleElement !== this.firstChild) {
|
|
191
|
+
this.insertBefore(styleElement, this.firstChild);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 恢复焦点状态(在 DOM 替换之后)
|
|
196
|
+
// 值已经在添加到 DOM 之前恢复了,这里只需要恢复焦点和光标位置
|
|
197
|
+
// 使用另一个 requestAnimationFrame 确保 DOM 已完全更新
|
|
198
|
+
requestAnimationFrame(() => {
|
|
199
|
+
this.restoreFocusState(focusState);
|
|
200
|
+
// 清除待处理的焦点状态
|
|
201
|
+
this._pendingFocusState = null;
|
|
202
|
+
});
|
|
203
|
+
});
|
|
138
204
|
} catch (error) {
|
|
139
205
|
logger.error(`[${this.constructor.name}] Error in rerender:`, error);
|
|
140
206
|
this.renderError(error);
|
|
@@ -53,11 +53,24 @@ export function state(target: unknown, propertyKey: string | symbol | unknown):
|
|
|
53
53
|
} else {
|
|
54
54
|
const propertyKeyStr = String(propertyKey);
|
|
55
55
|
if (propertyKeyStr === "[object Object]") {
|
|
56
|
-
// Invalid propertyKey -
|
|
56
|
+
// Invalid propertyKey - Babel plugin was not configured
|
|
57
57
|
throw new Error(
|
|
58
|
-
`@state decorator: Invalid propertyKey. ` +
|
|
59
|
-
`
|
|
60
|
-
`
|
|
58
|
+
`@state decorator: Invalid propertyKey detected. ` +
|
|
59
|
+
`\n\n` +
|
|
60
|
+
`The @state decorator MUST be processed by Babel plugin at compile time. ` +
|
|
61
|
+
`It appears the Babel plugin is not configured in your build setup.` +
|
|
62
|
+
`\n\n` +
|
|
63
|
+
`To fix this, please:` +
|
|
64
|
+
`\n1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin` +
|
|
65
|
+
`\n2. Configure it in vite.config.ts:` +
|
|
66
|
+
`\n import { wsx } from '@wsxjs/wsx-vite-plugin';` +
|
|
67
|
+
`\n export default defineConfig({ plugins: [wsx()] });` +
|
|
68
|
+
`\n3. Configure TypeScript (recommended: use @wsxjs/wsx-tsconfig):` +
|
|
69
|
+
`\n npm install --save-dev @wsxjs/wsx-tsconfig` +
|
|
70
|
+
`\n Then in tsconfig.json: { "extends": "@wsxjs/wsx-tsconfig/tsconfig.base.json" }` +
|
|
71
|
+
`\n Or manually: { "compilerOptions": { "experimentalDecorators": true, "useDefineForClassFields": false } }` +
|
|
72
|
+
`\n\n` +
|
|
73
|
+
`See: https://github.com/wsxjs/wsxjs#setup for more details.`
|
|
61
74
|
);
|
|
62
75
|
}
|
|
63
76
|
normalizedPropertyKey = propertyKeyStr;
|
|
@@ -71,8 +84,22 @@ export function state(target: unknown, propertyKey: string | symbol | unknown):
|
|
|
71
84
|
: normalizedPropertyKey.toString();
|
|
72
85
|
throw new Error(
|
|
73
86
|
`@state decorator: Cannot access property "${propertyKeyStr}". ` +
|
|
74
|
-
`Target is ${target === null ? "null" : "undefined"}
|
|
75
|
-
`
|
|
87
|
+
`Target is ${target === null ? "null" : "undefined"}.` +
|
|
88
|
+
`\n\n` +
|
|
89
|
+
`The @state decorator MUST be processed by Babel plugin at compile time. ` +
|
|
90
|
+
`It appears the Babel plugin is not configured in your build setup.` +
|
|
91
|
+
`\n\n` +
|
|
92
|
+
`To fix this, please:` +
|
|
93
|
+
`\n1. Install @wsxjs/wsx-vite-plugin: npm install @wsxjs/wsx-vite-plugin` +
|
|
94
|
+
`\n2. Configure it in vite.config.ts:` +
|
|
95
|
+
`\n import { wsx } from '@wsxjs/wsx-vite-plugin';` +
|
|
96
|
+
`\n export default defineConfig({ plugins: [wsx()] });` +
|
|
97
|
+
`\n3. Configure TypeScript (recommended: use @wsxjs/wsx-tsconfig):` +
|
|
98
|
+
`\n npm install --save-dev @wsxjs/wsx-tsconfig` +
|
|
99
|
+
`\n Then in tsconfig.json: { "extends": "@wsxjs/wsx-tsconfig/tsconfig.base.json" }` +
|
|
100
|
+
`\n Or manually: { "compilerOptions": { "experimentalDecorators": true, "useDefineForClassFields": false } }` +
|
|
101
|
+
`\n\n` +
|
|
102
|
+
`See: https://github.com/wsxjs/wsxjs#setup for more details.`
|
|
76
103
|
);
|
|
77
104
|
}
|
|
78
105
|
|