@wsxjs/wsx-core 0.0.7 → 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 +665 -487
- package/dist/index.mjs +657 -481
- 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 +549 -0
- package/src/index.ts +2 -4
- package/src/jsx-factory.ts +15 -0
- package/src/light-component.ts +102 -186
- package/src/reactive-decorator.ts +132 -0
- package/src/utils/reactive.ts +209 -35
- package/src/web-component.ts +89 -129
- package/types/index.d.ts +2 -2
- package/src/reactive-component.ts +0 -306
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
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Component for WSX Web Components
|
|
3
|
+
*
|
|
4
|
+
* Provides common functionality shared by WebComponent and LightComponent:
|
|
5
|
+
* - Reactive state management (reactive, useState, scheduleRerender)
|
|
6
|
+
* - Configuration management (getConfig, setConfig)
|
|
7
|
+
* - Attribute helpers (getAttr, setAttr, removeAttr, hasAttr)
|
|
8
|
+
* - Lifecycle hooks (onConnected, onDisconnected, onAttributeChanged)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { reactive as createReactive, createState, reactiveWithDebug } from "./utils/reactive";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Type for reactive state storage
|
|
15
|
+
*/
|
|
16
|
+
interface ReactiveStateStorage {
|
|
17
|
+
getter: () => unknown;
|
|
18
|
+
setter: (value: unknown | ((prev: unknown) => unknown)) => void;
|
|
19
|
+
}
|
|
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
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Base configuration interface
|
|
36
|
+
*/
|
|
37
|
+
export interface BaseComponentConfig {
|
|
38
|
+
styles?: string;
|
|
39
|
+
autoStyles?: string;
|
|
40
|
+
styleName?: string;
|
|
41
|
+
debug?: boolean;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Base Component class with common functionality
|
|
47
|
+
*
|
|
48
|
+
* This class provides shared functionality for both WebComponent and LightComponent.
|
|
49
|
+
* It should not be used directly - use WebComponent or LightComponent instead.
|
|
50
|
+
*/
|
|
51
|
+
export abstract class BaseComponent extends HTMLElement {
|
|
52
|
+
protected config: BaseComponentConfig;
|
|
53
|
+
protected connected: boolean = false;
|
|
54
|
+
protected _isDebugEnabled: boolean = false;
|
|
55
|
+
protected _reactiveStates = new Map<string, ReactiveStateStorage>();
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Auto-injected styles from Babel plugin (if CSS file exists)
|
|
59
|
+
* @internal - Managed by babel-plugin-wsx-style
|
|
60
|
+
*/
|
|
61
|
+
protected _autoStyles?: string;
|
|
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
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 子类应该重写这个方法来定义观察的属性
|
|
83
|
+
* @returns 要观察的属性名数组
|
|
84
|
+
*/
|
|
85
|
+
static get observedAttributes(): string[] {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
constructor(config: BaseComponentConfig = {}) {
|
|
90
|
+
super();
|
|
91
|
+
|
|
92
|
+
this._isDebugEnabled = config.debug ?? false;
|
|
93
|
+
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
95
|
+
const host = this;
|
|
96
|
+
|
|
97
|
+
// Store original styles value to avoid infinite recursion in getter
|
|
98
|
+
const originalStyles = config.styles;
|
|
99
|
+
|
|
100
|
+
this.config = {
|
|
101
|
+
...config,
|
|
102
|
+
get styles() {
|
|
103
|
+
// Auto-detect injected styles from class property
|
|
104
|
+
// Note: _defineProperty executes in constructor after super(),
|
|
105
|
+
// so we check _autoStyles dynamically via getter
|
|
106
|
+
// This works for both WebComponent and LightComponent
|
|
107
|
+
// Priority: originalStyles > _autoStyles
|
|
108
|
+
const result = originalStyles || host._autoStyles || "";
|
|
109
|
+
return result;
|
|
110
|
+
},
|
|
111
|
+
set styles(value: string) {
|
|
112
|
+
config.styles = value;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 抽象方法:子类必须实现JSX渲染
|
|
119
|
+
*
|
|
120
|
+
* @returns JSX元素
|
|
121
|
+
*/
|
|
122
|
+
abstract render(): HTMLElement;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 可选生命周期钩子:组件已连接
|
|
126
|
+
*/
|
|
127
|
+
protected onConnected?(): void;
|
|
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
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 可选生命周期钩子:组件已断开
|
|
160
|
+
*/
|
|
161
|
+
protected onDisconnected?(): void;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 可选生命周期钩子:属性已更改
|
|
165
|
+
*/
|
|
166
|
+
protected onAttributeChanged?(name: string, oldValue: string, newValue: string): void;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Web Component生命周期:属性变化
|
|
170
|
+
*/
|
|
171
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
|
|
172
|
+
this.onAttributeChanged?.(name, oldValue, newValue);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 创建响应式对象
|
|
177
|
+
*
|
|
178
|
+
* @param obj 要变为响应式的对象
|
|
179
|
+
* @param debugName 调试名称(可选)
|
|
180
|
+
* @returns 响应式代理对象
|
|
181
|
+
*/
|
|
182
|
+
protected reactive<T extends object>(obj: T, debugName?: string): T {
|
|
183
|
+
const reactiveFn = this._isDebugEnabled ? reactiveWithDebug : createReactive;
|
|
184
|
+
const name = debugName || `${this.constructor.name}.reactive`;
|
|
185
|
+
|
|
186
|
+
return this._isDebugEnabled
|
|
187
|
+
? reactiveFn(obj, () => this.scheduleRerender(), name)
|
|
188
|
+
: reactiveFn(obj, () => this.scheduleRerender());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 创建响应式状态
|
|
193
|
+
*
|
|
194
|
+
* @param key 状态标识符
|
|
195
|
+
* @param initialValue 初始值
|
|
196
|
+
* @returns [getter, setter] 元组
|
|
197
|
+
*/
|
|
198
|
+
protected useState<T>(
|
|
199
|
+
key: string,
|
|
200
|
+
initialValue: T
|
|
201
|
+
): [() => T, (value: T | ((prev: T) => T)) => void] {
|
|
202
|
+
if (!this._reactiveStates.has(key)) {
|
|
203
|
+
const [getter, setter] = createState(initialValue, () => this.scheduleRerender());
|
|
204
|
+
this._reactiveStates.set(key, {
|
|
205
|
+
getter: getter as () => unknown,
|
|
206
|
+
setter: setter as (value: unknown | ((prev: unknown) => unknown)) => void,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const state = this._reactiveStates.get(key);
|
|
211
|
+
if (!state) {
|
|
212
|
+
throw new Error(`State ${key} not found`);
|
|
213
|
+
}
|
|
214
|
+
return [state.getter as () => T, state.setter as (value: T | ((prev: T) => T)) => void];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 调度重渲染
|
|
219
|
+
* 这个方法被响应式系统调用,开发者通常不需要直接调用
|
|
220
|
+
* 使用 queueMicrotask 进行异步调度,与 reactive() 系统保持一致
|
|
221
|
+
*/
|
|
222
|
+
protected scheduleRerender(): void {
|
|
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;
|
|
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);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 重新渲染组件(子类需要实现)
|
|
300
|
+
*/
|
|
301
|
+
protected abstract rerender(): void;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 获取配置值
|
|
305
|
+
*
|
|
306
|
+
* @param key - 配置键
|
|
307
|
+
* @param defaultValue - 默认值
|
|
308
|
+
* @returns 配置值
|
|
309
|
+
*/
|
|
310
|
+
protected getConfig<T>(key: string, defaultValue?: T): T {
|
|
311
|
+
return (this.config[key] as T) ?? (defaultValue as T);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 设置配置值
|
|
316
|
+
*
|
|
317
|
+
* @param key - 配置键
|
|
318
|
+
* @param value - 配置值
|
|
319
|
+
*/
|
|
320
|
+
protected setConfig(key: string, value: unknown): void {
|
|
321
|
+
this.config[key] = value;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 获取属性值
|
|
326
|
+
*
|
|
327
|
+
* @param name - 属性名
|
|
328
|
+
* @param defaultValue - 默认值
|
|
329
|
+
* @returns 属性值
|
|
330
|
+
*/
|
|
331
|
+
protected getAttr(name: string, defaultValue = ""): string {
|
|
332
|
+
return this.getAttribute(name) || defaultValue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 设置属性值
|
|
337
|
+
*
|
|
338
|
+
* @param name - 属性名
|
|
339
|
+
* @param value - 属性值
|
|
340
|
+
*/
|
|
341
|
+
protected setAttr(name: string, value: string): void {
|
|
342
|
+
this.setAttribute(name, value);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* 移除属性
|
|
347
|
+
*
|
|
348
|
+
* @param name - 属性名
|
|
349
|
+
*/
|
|
350
|
+
protected removeAttr(name: string): void {
|
|
351
|
+
this.removeAttribute(name);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* 检查是否有属性
|
|
356
|
+
*
|
|
357
|
+
* @param name - 属性名
|
|
358
|
+
* @returns 是否存在
|
|
359
|
+
*/
|
|
360
|
+
protected hasAttr(name: string): boolean {
|
|
361
|
+
return this.hasAttribute(name);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 清理响应式状态
|
|
366
|
+
*/
|
|
367
|
+
protected cleanupReactiveStates(): void {
|
|
368
|
+
this._reactiveStates.clear();
|
|
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
|
+
}
|
|
549
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,9 +6,8 @@ export { h, h as jsx, h as jsxs, Fragment } from "./jsx-factory";
|
|
|
6
6
|
export { StyleManager } from "./styles/style-manager";
|
|
7
7
|
export { WSXLogger, logger, createLogger } from "./utils/logger";
|
|
8
8
|
|
|
9
|
-
// Reactive exports
|
|
10
|
-
export {
|
|
11
|
-
export { ReactiveWebComponent, makeReactive, createReactiveComponent } from "./reactive-component";
|
|
9
|
+
// Reactive exports - Decorator-based API
|
|
10
|
+
export { state } from "./reactive-decorator";
|
|
12
11
|
|
|
13
12
|
// Type exports
|
|
14
13
|
export type { WebComponentConfig } from "./web-component";
|
|
@@ -16,4 +15,3 @@ export type { LightComponentConfig } from "./light-component";
|
|
|
16
15
|
export type { JSXChildren } from "./jsx-factory";
|
|
17
16
|
export type { Logger, LogLevel } from "./utils/logger";
|
|
18
17
|
export type { ReactiveCallback } from "./utils/reactive";
|
|
19
|
-
export type { ReactiveWebComponentConfig } from "./reactive-component";
|
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元素使用正确的属性名
|