@wsxjs/wsx-core 0.0.6 → 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.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Reactive Decorators for WSX Components
3
+ *
4
+ * Provides @state property decorator to mark properties as reactive state.
5
+ * WebComponent and LightComponent already have reactive() and useState() methods.
6
+ * Babel plugin handles @state initialization at compile time.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * class Counter extends WebComponent {
11
+ * @state private count = 0;
12
+ * @state private user = { name: "John" };
13
+ * }
14
+ * ```
15
+ */
16
+
17
+ /**
18
+ * State property decorator
19
+ *
20
+ * Marks a property as reactive state. Babel plugin processes this decorator at compile time
21
+ * and generates initialization code in the constructor.
22
+ *
23
+ * Automatically uses reactive() for objects/arrays and useState() for primitives.
24
+ *
25
+ * @param target - The class prototype
26
+ * @param propertyKey - The property name
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * class MyComponent extends WebComponent {
31
+ * @state private count = 0; // Auto-initialized by Babel plugin
32
+ * @state private user = { name: "John" }; // Auto-initialized by Babel plugin
33
+ * }
34
+ * ```
35
+ */
36
+ export function state(target: unknown, propertyKey: string | symbol | unknown): void {
37
+ /**
38
+ * @state decorator is a compile-time marker for Babel plugin.
39
+ * Babel plugin will:
40
+ * 1. Detect @state decorator on properties
41
+ * 2. Extract initial value from AST
42
+ * 3. Remove @state decorator
43
+ * 4. Generate initialization code in constructor (this.state = this.reactive(...) or useState)
44
+ *
45
+ * This runtime function only performs basic validation.
46
+ * If Babel plugin is not configured, this will throw an error.
47
+ */
48
+
49
+ // Normalize propertyKey
50
+ let normalizedPropertyKey: string | symbol;
51
+ if (typeof propertyKey === "string" || typeof propertyKey === "symbol") {
52
+ normalizedPropertyKey = propertyKey;
53
+ } else {
54
+ const propertyKeyStr = String(propertyKey);
55
+ if (propertyKeyStr === "[object Object]") {
56
+ // Invalid propertyKey - likely a build configuration issue
57
+ throw new Error(
58
+ `@state decorator: Invalid propertyKey. ` +
59
+ `This usually means the build tool doesn't support decorators properly. ` +
60
+ `Please ensure Babel plugin is configured in vite.config.ts`
61
+ );
62
+ }
63
+ normalizedPropertyKey = propertyKeyStr;
64
+ }
65
+
66
+ // Basic validation: ensure target is valid
67
+ if (target == null) {
68
+ const propertyKeyStr =
69
+ typeof normalizedPropertyKey === "string"
70
+ ? normalizedPropertyKey
71
+ : normalizedPropertyKey.toString();
72
+ throw new Error(
73
+ `@state decorator: Cannot access property "${propertyKeyStr}". ` +
74
+ `Target is ${target === null ? "null" : "undefined"}. ` +
75
+ `Please ensure Babel plugin is configured in vite.config.ts`
76
+ );
77
+ }
78
+
79
+ if (typeof target !== "object") {
80
+ const propertyKeyStr =
81
+ typeof normalizedPropertyKey === "string"
82
+ ? normalizedPropertyKey
83
+ : normalizedPropertyKey.toString();
84
+ throw new Error(
85
+ `@state decorator: Cannot be used on "${propertyKeyStr}". ` +
86
+ `@state is for properties only, not methods.`
87
+ );
88
+ }
89
+
90
+ // Validate that property has an initial value
91
+ const descriptor = Object.getOwnPropertyDescriptor(target, normalizedPropertyKey);
92
+ if (descriptor?.get) {
93
+ const propertyKeyStr =
94
+ typeof normalizedPropertyKey === "string"
95
+ ? normalizedPropertyKey
96
+ : normalizedPropertyKey.toString();
97
+ throw new Error(
98
+ `@state decorator cannot be used with getter properties. Property: "${propertyKeyStr}"`
99
+ );
100
+ }
101
+
102
+ // Note: We don't store metadata or remove the property here.
103
+ // Babel plugin handles everything at compile time.
104
+ // If this function is called at runtime, it means Babel plugin didn't process the decorator.
105
+ }
@@ -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
- styles?: string; // CSS内容
19
- styleName?: string; // 样式名称,用于缓存
20
- [key: string]: unknown;
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 HTMLElement {
36
+ export abstract class WebComponent extends BaseComponent {
27
37
  declare shadowRoot: ShadowRoot;
28
- protected config: WebComponentConfig;
29
- protected connected: boolean = false;
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
- this.config = config;
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
- // 自动应用CSS样式
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
- if (adoptedStyleSheets.length === 0 && this.config.styles) {
149
- const styleName = this.config.styleName || this.constructor.name;
150
- StyleManager.applyStyles(this.shadowRoot, styleName, this.config.styles);
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
- // 导出响应式 WebComponent 类和配置
14
- export { ReactiveWebComponent, type ReactiveWebComponentConfig } from "../src/reactive-component";
13
+ // 导出响应式装饰器 (@state)
14
+ export { state } from "../src/reactive-decorator";
15
15
 
16
16
  // 导出 LightComponent 类和配置
17
17
  export { LightComponent, type LightComponentConfig } from "../src/light-component";
@@ -2,10 +2,12 @@
2
2
  * WSX TypeScript 声明文件
3
3
  * 支持 JSX 语法和其他 WSX 特性
4
4
  */
5
-
5
+ import { WebComponent, LightComponent } from "../src/index";
6
6
  // WSX 文件支持 - 将 .wsx 文件视为 TypeScript 模块
7
+ // 标准类型声明:支持 WebComponent 和 LightComponent
7
8
  declare module "*.wsx" {
8
- const Component: unknown;
9
+ // Allow any class that extends WebComponent or LightComponent
10
+ const Component: new (...args: unknown[]) => WebComponent | LightComponent;
9
11
  export default Component;
10
12
  }
11
13