@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.
- package/LICENSE +21 -0
- package/dist/index.js +299 -411
- package/dist/index.mjs +297 -404
- package/package.json +46 -46
- package/src/base-component.ts +239 -0
- package/src/index.ts +2 -4
- package/src/light-component.ts +38 -165
- package/src/reactive-decorator.ts +105 -0
- package/src/web-component.ts +157 -110
- package/types/index.d.ts +2 -2
- package/types/wsx-types.d.ts +4 -2
- package/src/reactive-component.ts +0 -306
|
@@ -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
|
+
}
|
package/src/web-component.ts
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
36
|
+
export abstract class WebComponent extends BaseComponent {
|
|
27
37
|
declare shadowRoot: ShadowRoot;
|
|
28
|
-
protected config
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
//
|
|
14
|
-
export {
|
|
13
|
+
// 导出响应式装饰器 (@state)
|
|
14
|
+
export { state } from "../src/reactive-decorator";
|
|
15
15
|
|
|
16
16
|
// 导出 LightComponent 类和配置
|
|
17
17
|
export { LightComponent, type LightComponentConfig } from "../src/light-component";
|
package/types/wsx-types.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|