@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.
@@ -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));
@@ -2,7 +2,7 @@ import {
2
2
  Fragment,
3
3
  jsx,
4
4
  jsxs
5
- } from "./chunk-VZQT7HU5.mjs";
5
+ } from "./chunk-CZII6RG2.mjs";
6
6
  export {
7
7
  Fragment,
8
8
  jsx,
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
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Fragment,
3
3
  h
4
- } from "./chunk-VZQT7HU5.mjs";
4
+ } from "./chunk-CZII6RG2.mjs";
5
5
  export {
6
6
  Fragment,
7
7
  h
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wsxjs/wsx-core",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Core WSX Framework - Web Components with JSX syntax",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -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 { reactive, createState, ReactiveDebug } from "./utils/reactive";
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";
@@ -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元素使用正确的属性名