@wsxjs/wsx-core 0.0.17 → 0.0.18
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 +2 -2
- package/dist/chunk-7AUYPTR5.mjs +277 -0
- package/dist/chunk-7WRSZQC3.mjs +256 -0
- package/dist/chunk-CBCT3PYF.mjs +250 -0
- package/dist/chunk-G23NXAPQ.mjs +237 -0
- package/dist/chunk-GOIGP2BR.mjs +271 -0
- package/dist/chunk-H6LR3P4O.mjs +256 -0
- package/dist/chunk-JV57DWHH.mjs +265 -0
- package/dist/chunk-UH5BDYGI.mjs +283 -0
- package/dist/index.js +170 -71
- package/dist/index.mjs +113 -69
- package/dist/jsx-runtime.js +56 -2
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +56 -2
- package/dist/jsx.mjs +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +15 -2
- package/src/base-component.ts +60 -11
- package/src/index.ts +3 -2
- package/src/jsx-factory.ts +80 -2
- package/src/light-component.ts +93 -28
- package/src/utils/dom-utils.ts +48 -0
- package/src/utils/logger.ts +2 -2
- package/src/utils/reactive.ts +1 -1
- package/src/web-component.ts +37 -22
- package/types/index.d.ts +6 -3
package/src/base-component.ts
CHANGED
|
@@ -78,6 +78,12 @@ export abstract class BaseComponent extends HTMLElement {
|
|
|
78
78
|
*/
|
|
79
79
|
private _pendingRerender: boolean = false;
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* 正在渲染标志(防止在 _rerender() 执行期间再次触发 scheduleRerender())
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
85
|
+
protected _isRendering: boolean = false;
|
|
86
|
+
|
|
81
87
|
/**
|
|
82
88
|
* 子类应该重写这个方法来定义观察的属性
|
|
83
89
|
* @returns 要观察的属性名数组
|
|
@@ -126,6 +132,12 @@ export abstract class BaseComponent extends HTMLElement {
|
|
|
126
132
|
*/
|
|
127
133
|
protected onConnected?(): void;
|
|
128
134
|
|
|
135
|
+
/**
|
|
136
|
+
* 可选生命周期钩子:组件渲染完成后调用
|
|
137
|
+
* 在 DOM 更新完成后调用,适合执行需要访问 DOM 的操作(如语法高亮、初始化第三方库等)
|
|
138
|
+
*/
|
|
139
|
+
protected onRendered?(): void;
|
|
140
|
+
|
|
129
141
|
/**
|
|
130
142
|
* 处理 blur 事件,在用户停止输入时执行待处理的重渲染
|
|
131
143
|
* @internal
|
|
@@ -146,9 +158,14 @@ export abstract class BaseComponent extends HTMLElement {
|
|
|
146
158
|
|
|
147
159
|
// 延迟一小段时间后重渲染,确保 blur 事件完全处理
|
|
148
160
|
requestAnimationFrame(() => {
|
|
149
|
-
if (this._pendingRerender && this.connected) {
|
|
161
|
+
if (this._pendingRerender && this.connected && !this._isRendering) {
|
|
150
162
|
this._pendingRerender = false;
|
|
151
|
-
|
|
163
|
+
// 设置渲染标志,防止在 _rerender() 执行期间再次触发
|
|
164
|
+
// 注意:_isRendering 标志会在 _rerender() 的 onRendered() 调用完成后清除
|
|
165
|
+
this._isRendering = true;
|
|
166
|
+
// 调用 _rerender() 执行实际渲染(不再调用 rerender(),避免循环)
|
|
167
|
+
// _isRendering 标志会在 _rerender() 完成所有异步操作后清除
|
|
168
|
+
this._rerender();
|
|
152
169
|
}
|
|
153
170
|
});
|
|
154
171
|
}
|
|
@@ -229,6 +246,11 @@ export abstract class BaseComponent extends HTMLElement {
|
|
|
229
246
|
return;
|
|
230
247
|
}
|
|
231
248
|
|
|
249
|
+
// 如果正在渲染,跳过本次调度(防止无限循环)
|
|
250
|
+
if (this._isRendering) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
232
254
|
// 检查是否有需要持续输入的元素获得焦点(input、textarea、select、contenteditable)
|
|
233
255
|
// 按钮等其他元素应该立即重渲染,以反映状态变化
|
|
234
256
|
const root = this.getActiveRoot();
|
|
@@ -273,18 +295,50 @@ export abstract class BaseComponent extends HTMLElement {
|
|
|
273
295
|
// 对于按钮等其他元素,或者有 data-wsx-force-render 属性的输入元素,继续执行重渲染(不跳过)
|
|
274
296
|
}
|
|
275
297
|
|
|
276
|
-
// 没有焦点元素,立即重渲染(使用
|
|
298
|
+
// 没有焦点元素,立即重渲染(使用 requestAnimationFrame 确保在下一个渲染帧执行)
|
|
277
299
|
// 如果有待处理的重渲染,也立即执行
|
|
278
300
|
if (this._pendingRerender) {
|
|
279
301
|
this._pendingRerender = false;
|
|
280
302
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
303
|
+
// 使用 requestAnimationFrame 而不是 queueMicrotask,确保在渲染帧中执行
|
|
304
|
+
// 这样可以避免在 render() 执行期间触发的 scheduleRerender() 立即执行
|
|
305
|
+
requestAnimationFrame(() => {
|
|
306
|
+
if (this.connected && !this._isRendering) {
|
|
307
|
+
// 设置渲染标志,防止在 _rerender() 执行期间再次触发
|
|
308
|
+
// 注意:_isRendering 标志会在 _rerender() 的 onRendered() 调用完成后清除
|
|
309
|
+
this._isRendering = true;
|
|
310
|
+
// 调用 _rerender() 执行实际渲染(不再调用 rerender(),避免循环)
|
|
311
|
+
// _isRendering 标志会在 _rerender() 完成所有异步操作后清除
|
|
312
|
+
this._rerender();
|
|
313
|
+
} else if (!this.connected) {
|
|
314
|
+
// 如果组件已断开,确保清除渲染标志
|
|
315
|
+
this._isRendering = false;
|
|
284
316
|
}
|
|
285
317
|
});
|
|
286
318
|
}
|
|
287
319
|
|
|
320
|
+
/**
|
|
321
|
+
* 调度重渲染(公开 API)
|
|
322
|
+
*
|
|
323
|
+
* 与 scheduleRerender() 对齐:所有重渲染都通过统一的调度机制
|
|
324
|
+
* 使用异步调度机制,自动处理防抖和批量更新
|
|
325
|
+
*
|
|
326
|
+
* 注意:此方法现在是异步的,使用调度机制
|
|
327
|
+
* 如果需要同步执行,使用 _rerender()(不推荐,仅内部使用)
|
|
328
|
+
*/
|
|
329
|
+
protected rerender(): void {
|
|
330
|
+
// 对齐到 scheduleRerender(),统一调度机制
|
|
331
|
+
this.scheduleRerender();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 内部重渲染实现(同步执行)
|
|
336
|
+
* 由 scheduleRerender() 在适当时机调用
|
|
337
|
+
*
|
|
338
|
+
* @internal - 子类需要实现此方法
|
|
339
|
+
*/
|
|
340
|
+
protected abstract _rerender(): void;
|
|
341
|
+
|
|
288
342
|
/**
|
|
289
343
|
* 清理资源(在组件断开连接时调用)
|
|
290
344
|
* @internal
|
|
@@ -312,11 +366,6 @@ export abstract class BaseComponent extends HTMLElement {
|
|
|
312
366
|
document.addEventListener("blur", this.handleGlobalBlur, true);
|
|
313
367
|
}
|
|
314
368
|
|
|
315
|
-
/**
|
|
316
|
-
* 重新渲染组件(子类需要实现)
|
|
317
|
-
*/
|
|
318
|
-
protected abstract rerender(): void;
|
|
319
|
-
|
|
320
369
|
/**
|
|
321
370
|
* 获取配置值
|
|
322
371
|
*
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,9 @@ export { LightComponent } from "./light-component";
|
|
|
4
4
|
export { autoRegister, registerComponent } from "./auto-register";
|
|
5
5
|
export { h, h as jsx, h as jsxs, Fragment } from "./jsx-factory";
|
|
6
6
|
export { StyleManager } from "./styles/style-manager";
|
|
7
|
-
export
|
|
7
|
+
// Re-export logger from wsx-logger for backward compatibility
|
|
8
|
+
export { WSXLogger, logger, createLogger, createLoggerWithConfig } from "@wsxjs/wsx-logger";
|
|
9
|
+
export type { Logger, LogLevel } from "@wsxjs/wsx-logger";
|
|
8
10
|
|
|
9
11
|
// Reactive exports - Decorator-based API
|
|
10
12
|
export { state } from "./reactive-decorator";
|
|
@@ -13,5 +15,4 @@ export { state } from "./reactive-decorator";
|
|
|
13
15
|
export type { WebComponentConfig } from "./web-component";
|
|
14
16
|
export type { LightComponentConfig } from "./light-component";
|
|
15
17
|
export type { JSXChildren } from "./jsx-factory";
|
|
16
|
-
export type { Logger, LogLevel } from "./utils/logger";
|
|
17
18
|
export type { ReactiveCallback } from "./utils/reactive";
|
package/src/jsx-factory.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// JSX 类型声明已移至 types/wsx-types.d.ts
|
|
13
13
|
|
|
14
14
|
import { createElement, shouldUseSVGNamespace, getSVGAttributeName } from "./utils/svg-utils";
|
|
15
|
+
import { parseHTMLToNodes } from "./utils/dom-utils";
|
|
15
16
|
|
|
16
17
|
// JSX子元素类型
|
|
17
18
|
export type JSXChildren =
|
|
@@ -132,12 +133,52 @@ export function h(
|
|
|
132
133
|
return element;
|
|
133
134
|
}
|
|
134
135
|
|
|
136
|
+
/**
|
|
137
|
+
* 检测字符串是否包含HTML标签
|
|
138
|
+
* 使用更严格的检测:必须包含完整的 HTML 标签(开始和结束标签,或自闭合标签)
|
|
139
|
+
*/
|
|
140
|
+
function isHTMLString(str: string): boolean {
|
|
141
|
+
const trimmed = str.trim();
|
|
142
|
+
if (!trimmed) return false;
|
|
143
|
+
|
|
144
|
+
// 更严格的检测:必须包含完整的 HTML 标签
|
|
145
|
+
// 1. 必须以 < 开头
|
|
146
|
+
// 2. 后面跟着字母(标签名)
|
|
147
|
+
// 3. 必须包含 > 来闭合标签
|
|
148
|
+
// 4. 排除单独的 < 或 > 符号(如数学表达式 "a < b")
|
|
149
|
+
const htmlTagPattern = /<[a-z][a-z0-9]*(\s[^>]*)?(\/>|>)/i;
|
|
150
|
+
|
|
151
|
+
// 额外检查:确保不是纯文本中的 < 和 >(如 "a < b" 或 "x > y")
|
|
152
|
+
// 如果字符串看起来像数学表达式或纯文本,不应该被检测为 HTML
|
|
153
|
+
const looksLikeMath = /^[^<]*<[^>]*>[^>]*$/.test(trimmed) && !htmlTagPattern.test(trimmed);
|
|
154
|
+
if (looksLikeMath) return false;
|
|
155
|
+
|
|
156
|
+
return htmlTagPattern.test(trimmed);
|
|
157
|
+
}
|
|
158
|
+
|
|
135
159
|
/**
|
|
136
160
|
* 扁平化子元素数组
|
|
161
|
+
* 自动检测HTML字符串并转换为DOM节点
|
|
162
|
+
*
|
|
163
|
+
* @param children - 子元素数组
|
|
164
|
+
* @param skipHTMLDetection - 是否跳过HTML检测(用于已解析的节点,避免无限递归)
|
|
165
|
+
* @param depth - 当前递归深度(防止无限递归,最大深度为 10)
|
|
137
166
|
*/
|
|
138
167
|
function flattenChildren(
|
|
139
|
-
children: JSXChildren[]
|
|
168
|
+
children: JSXChildren[],
|
|
169
|
+
skipHTMLDetection: boolean = false,
|
|
170
|
+
depth: number = 0
|
|
140
171
|
): (string | number | HTMLElement | SVGElement | DocumentFragment | boolean | null | undefined)[] {
|
|
172
|
+
// 防止无限递归:如果深度超过 10,停止处理
|
|
173
|
+
if (depth > 10) {
|
|
174
|
+
console.warn(
|
|
175
|
+
"[WSX] flattenChildren: Maximum depth exceeded, treating remaining children as text"
|
|
176
|
+
);
|
|
177
|
+
return children.filter(
|
|
178
|
+
(child): child is string | number =>
|
|
179
|
+
typeof child === "string" || typeof child === "number"
|
|
180
|
+
);
|
|
181
|
+
}
|
|
141
182
|
const result: (
|
|
142
183
|
| string
|
|
143
184
|
| number
|
|
@@ -153,7 +194,44 @@ function flattenChildren(
|
|
|
153
194
|
if (child === null || child === undefined || child === false) {
|
|
154
195
|
continue;
|
|
155
196
|
} else if (Array.isArray(child)) {
|
|
156
|
-
|
|
197
|
+
// 递归处理数组,保持 skipHTMLDetection 状态,增加深度
|
|
198
|
+
result.push(...flattenChildren(child, skipHTMLDetection, depth + 1));
|
|
199
|
+
} else if (typeof child === "string") {
|
|
200
|
+
// 如果跳过HTML检测,直接添加字符串(避免无限递归)
|
|
201
|
+
if (skipHTMLDetection) {
|
|
202
|
+
result.push(child);
|
|
203
|
+
} else if (isHTMLString(child)) {
|
|
204
|
+
// 自动检测HTML字符串并转换为DOM节点
|
|
205
|
+
// 使用 try-catch 防止解析失败导致崩溃
|
|
206
|
+
try {
|
|
207
|
+
const nodes = parseHTMLToNodes(child);
|
|
208
|
+
// 递归处理转换后的节点数组,标记为已解析,避免再次检测HTML
|
|
209
|
+
// parseHTMLToNodes 返回的字符串是纯文本节点,不应该再次被检测为HTML
|
|
210
|
+
// 但是为了安全,我们仍然设置 skipHTMLDetection = true
|
|
211
|
+
if (nodes.length > 0) {
|
|
212
|
+
// 直接添加解析后的节点,不再递归处理(避免无限递归)
|
|
213
|
+
// parseHTMLToNodes 已经完成了所有解析工作
|
|
214
|
+
for (const node of nodes) {
|
|
215
|
+
if (typeof node === "string") {
|
|
216
|
+
// 文本节点直接添加,不再检测 HTML(已解析)
|
|
217
|
+
result.push(node);
|
|
218
|
+
} else {
|
|
219
|
+
// DOM 元素直接添加
|
|
220
|
+
result.push(node);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
// 如果解析失败,回退到纯文本
|
|
225
|
+
result.push(child);
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
// 如果解析失败,回退到纯文本,避免崩溃
|
|
229
|
+
console.warn("[WSX] Failed to parse HTML string, treating as text:", error);
|
|
230
|
+
result.push(child);
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
result.push(child);
|
|
234
|
+
}
|
|
157
235
|
} else {
|
|
158
236
|
result.push(child);
|
|
159
237
|
}
|
package/src/light-component.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { h, type JSXChildren } from "./jsx-factory";
|
|
11
11
|
import { BaseComponent, type BaseComponentConfig } from "./base-component";
|
|
12
|
-
import { createLogger } from "
|
|
12
|
+
import { createLogger } from "@wsxjs/wsx-logger";
|
|
13
13
|
|
|
14
14
|
const logger = createLogger("LightComponent");
|
|
15
15
|
|
|
@@ -59,8 +59,9 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
59
59
|
this.applyScopedStyles(styleName, stylesToApply);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
//
|
|
62
|
+
// 检查是否有实际内容(排除样式元素和 slot 元素)
|
|
63
63
|
// 错误元素:如果存在错误信息,需要重新渲染以恢复正常
|
|
64
|
+
// Slot 元素:不算"内容",因为 slot 的内容在 Light DOM 中(通过 JSX children 传递)
|
|
64
65
|
const styleElement = this.querySelector(
|
|
65
66
|
`style[data-wsx-light-component="${styleName}"]`
|
|
66
67
|
) as HTMLStyleElement | null;
|
|
@@ -71,14 +72,16 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
71
72
|
child.style.color === "red" &&
|
|
72
73
|
child.textContent?.includes("Component Error")
|
|
73
74
|
);
|
|
75
|
+
// 排除样式元素和 slot 元素
|
|
74
76
|
const hasActualContent = Array.from(this.children).some(
|
|
75
|
-
(child) => child !== styleElement
|
|
77
|
+
(child) => child !== styleElement && !(child instanceof HTMLSlotElement)
|
|
76
78
|
);
|
|
77
79
|
|
|
78
80
|
// 如果有错误元素,需要重新渲染以恢复正常
|
|
79
81
|
// 如果有实际内容且没有错误,跳过渲染(避免重复元素)
|
|
80
82
|
if (hasActualContent && !hasErrorElement) {
|
|
81
|
-
//
|
|
83
|
+
// 已经有内容(JSX children),标记它们
|
|
84
|
+
this.markJSXChildren(); // 标记 JSX children,以便在 _rerender() 中保留
|
|
82
85
|
// 但确保样式元素在正确位置
|
|
83
86
|
if (styleElement && styleElement !== this.firstChild) {
|
|
84
87
|
this.insertBefore(styleElement, this.firstChild);
|
|
@@ -106,6 +109,14 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
106
109
|
|
|
107
110
|
// 调用子类的初始化钩子(无论是否渲染,都需要调用,因为组件已连接)
|
|
108
111
|
this.onConnected?.();
|
|
112
|
+
|
|
113
|
+
// 如果进行了渲染,调用 onRendered 钩子
|
|
114
|
+
if (hasActualContent === false || hasErrorElement) {
|
|
115
|
+
// 使用 requestAnimationFrame 确保 DOM 已完全更新
|
|
116
|
+
requestAnimationFrame(() => {
|
|
117
|
+
this.onRendered?.();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
109
120
|
} catch (error) {
|
|
110
121
|
logger.error(`[${this.constructor.name}] Error in connectedCallback:`, error);
|
|
111
122
|
this.renderError(error);
|
|
@@ -144,29 +155,33 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
/**
|
|
147
|
-
*
|
|
158
|
+
* 内部重渲染实现
|
|
159
|
+
* 包含从 rerender() 方法迁移的实际渲染逻辑
|
|
160
|
+
* 处理 JSX children 的保留(Light DOM 特有)
|
|
161
|
+
*
|
|
162
|
+
* @override
|
|
148
163
|
*/
|
|
149
|
-
protected
|
|
164
|
+
protected _rerender(): void {
|
|
150
165
|
if (!this.connected) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
);
|
|
166
|
+
// 如果组件未连接,清除渲染标志
|
|
167
|
+
this._isRendering = false;
|
|
154
168
|
return;
|
|
155
169
|
}
|
|
156
170
|
|
|
157
171
|
// 1. 捕获焦点状态(在 DOM 替换之前)
|
|
158
172
|
const focusState = this.captureFocusState();
|
|
159
|
-
// 保存到实例变量,供 render() 使用(如果需要)
|
|
160
173
|
this._pendingFocusState = focusState;
|
|
161
174
|
|
|
175
|
+
// 2. 保存 JSX children(通过 JSX factory 直接添加的 children)
|
|
176
|
+
// 这些 children 不是 render() 返回的内容,应该保留
|
|
177
|
+
const jsxChildren = this.getJSXChildren();
|
|
178
|
+
|
|
162
179
|
try {
|
|
163
|
-
// 重新渲染JSX内容
|
|
180
|
+
// 3. 重新渲染JSX内容
|
|
164
181
|
const content = this.render();
|
|
165
182
|
|
|
166
|
-
// 在添加到 DOM 之前恢复值,避免浏览器渲染状态值
|
|
167
|
-
// 这样可以确保值在元素添加到 DOM 之前就是正确的
|
|
183
|
+
// 4. 在添加到 DOM 之前恢复值,避免浏览器渲染状态值
|
|
168
184
|
if (focusState && focusState.key && focusState.value !== undefined) {
|
|
169
|
-
// 在 content 树中查找目标元素
|
|
170
185
|
const target = content.querySelector(
|
|
171
186
|
`[data-wsx-key="${focusState.key}"]`
|
|
172
187
|
) as HTMLElement;
|
|
@@ -181,10 +196,10 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
181
196
|
}
|
|
182
197
|
}
|
|
183
198
|
|
|
184
|
-
// 确保样式元素存在
|
|
199
|
+
// 5. 确保样式元素存在
|
|
185
200
|
const stylesToApply = this._autoStyles || this.config.styles;
|
|
201
|
+
const styleName = this.config.styleName || this.constructor.name;
|
|
186
202
|
if (stylesToApply) {
|
|
187
|
-
const styleName = this.config.styleName || this.constructor.name;
|
|
188
203
|
let styleElement = this.querySelector(
|
|
189
204
|
`style[data-wsx-light-component="${styleName}"]`
|
|
190
205
|
) as HTMLStyleElement;
|
|
@@ -201,27 +216,29 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
201
216
|
}
|
|
202
217
|
}
|
|
203
218
|
|
|
204
|
-
// 使用 requestAnimationFrame 批量执行 DOM
|
|
205
|
-
// 在同一帧中完成添加和移除,避免中间状态
|
|
219
|
+
// 6. 使用 requestAnimationFrame 批量执行 DOM 操作
|
|
206
220
|
requestAnimationFrame(() => {
|
|
207
221
|
// 先添加新内容
|
|
208
222
|
this.appendChild(content);
|
|
209
223
|
|
|
210
|
-
//
|
|
224
|
+
// 移除旧内容(保留 JSX children 和样式元素)
|
|
211
225
|
const oldChildren = Array.from(this.children).filter((child) => {
|
|
212
226
|
// 保留新添加的内容
|
|
213
227
|
if (child === content) {
|
|
214
228
|
return false;
|
|
215
229
|
}
|
|
216
|
-
//
|
|
230
|
+
// 保留样式元素
|
|
217
231
|
if (
|
|
218
232
|
stylesToApply &&
|
|
219
233
|
child instanceof HTMLStyleElement &&
|
|
220
|
-
child.getAttribute("data-wsx-light-component") ===
|
|
221
|
-
(this.config.styleName || this.constructor.name)
|
|
234
|
+
child.getAttribute("data-wsx-light-component") === styleName
|
|
222
235
|
) {
|
|
223
236
|
return false;
|
|
224
237
|
}
|
|
238
|
+
// 保留 JSX children(关键修复)
|
|
239
|
+
if (child instanceof HTMLElement && jsxChildren.includes(child)) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
225
242
|
return true;
|
|
226
243
|
});
|
|
227
244
|
oldChildren.forEach((child) => child.remove());
|
|
@@ -229,28 +246,74 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
229
246
|
// 确保样式元素在第一个位置
|
|
230
247
|
if (stylesToApply && this.children.length > 1) {
|
|
231
248
|
const styleElement = this.querySelector(
|
|
232
|
-
`style[data-wsx-light-component="${
|
|
249
|
+
`style[data-wsx-light-component="${styleName}"]`
|
|
233
250
|
);
|
|
234
251
|
if (styleElement && styleElement !== this.firstChild) {
|
|
235
252
|
this.insertBefore(styleElement, this.firstChild);
|
|
236
253
|
}
|
|
237
254
|
}
|
|
238
255
|
|
|
239
|
-
//
|
|
240
|
-
// 值已经在添加到 DOM 之前恢复了,这里只需要恢复焦点和光标位置
|
|
241
|
-
// 使用另一个 requestAnimationFrame 确保 DOM 已完全更新
|
|
256
|
+
// 恢复焦点状态
|
|
242
257
|
requestAnimationFrame(() => {
|
|
243
258
|
this.restoreFocusState(focusState);
|
|
244
|
-
// 清除待处理的焦点状态
|
|
245
259
|
this._pendingFocusState = null;
|
|
260
|
+
// 调用 onRendered 生命周期钩子
|
|
261
|
+
this.onRendered?.();
|
|
262
|
+
// 在 onRendered() 完成后清除渲染标志,允许后续的 scheduleRerender()
|
|
263
|
+
this._isRendering = false;
|
|
246
264
|
});
|
|
247
265
|
});
|
|
248
266
|
} catch (error) {
|
|
249
|
-
logger.error(`[${this.constructor.name}] Error in
|
|
267
|
+
logger.error(`[${this.constructor.name}] Error in _rerender:`, error);
|
|
250
268
|
this.renderError(error);
|
|
269
|
+
// 即使出错也要清除渲染标志,允许后续的 scheduleRerender()
|
|
270
|
+
this._isRendering = false;
|
|
251
271
|
}
|
|
252
272
|
}
|
|
253
273
|
|
|
274
|
+
/**
|
|
275
|
+
* 获取 JSX children(通过 JSX factory 直接添加的 children)
|
|
276
|
+
*
|
|
277
|
+
* 在 Light DOM 中,JSX children 是通过 JSX factory 直接添加到组件元素的
|
|
278
|
+
* 这些 children 不是 render() 返回的内容,应该保留
|
|
279
|
+
*/
|
|
280
|
+
private getJSXChildren(): HTMLElement[] {
|
|
281
|
+
// 在 connectedCallback 中标记的 JSX children
|
|
282
|
+
// 使用 data 属性标记:data-wsx-jsx-child="true"
|
|
283
|
+
const jsxChildren = Array.from(this.children)
|
|
284
|
+
.filter(
|
|
285
|
+
(child) =>
|
|
286
|
+
child instanceof HTMLElement &&
|
|
287
|
+
child.getAttribute("data-wsx-jsx-child") === "true"
|
|
288
|
+
)
|
|
289
|
+
.map((child) => child as HTMLElement);
|
|
290
|
+
|
|
291
|
+
return jsxChildren;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 标记 JSX children(在 connectedCallback 中调用)
|
|
296
|
+
*/
|
|
297
|
+
private markJSXChildren(): void {
|
|
298
|
+
// 在 connectedCallback 中,如果 hasActualContent 为 true
|
|
299
|
+
// 说明这些 children 是 JSX children,不是 render() 返回的内容
|
|
300
|
+
// 标记它们,以便在 _rerender() 中保留
|
|
301
|
+
const styleName = this.config.styleName || this.constructor.name;
|
|
302
|
+
const styleElement = this.querySelector(
|
|
303
|
+
`style[data-wsx-light-component="${styleName}"]`
|
|
304
|
+
) as HTMLStyleElement | null;
|
|
305
|
+
|
|
306
|
+
Array.from(this.children).forEach((child) => {
|
|
307
|
+
if (
|
|
308
|
+
child instanceof HTMLElement &&
|
|
309
|
+
child !== styleElement &&
|
|
310
|
+
!(child instanceof HTMLSlotElement)
|
|
311
|
+
) {
|
|
312
|
+
child.setAttribute("data-wsx-jsx-child", "true");
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
254
317
|
/**
|
|
255
318
|
* 渲染错误信息
|
|
256
319
|
*
|
|
@@ -258,6 +321,8 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
258
321
|
*/
|
|
259
322
|
private renderError(error: unknown): void {
|
|
260
323
|
// 清空现有内容
|
|
324
|
+
// Note: innerHTML is used here for framework-level error handling
|
|
325
|
+
// This is an exception to the no-inner-html rule for framework code
|
|
261
326
|
this.innerHTML = "";
|
|
262
327
|
|
|
263
328
|
const errorElement = h(
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM utilities for WSX
|
|
3
|
+
*
|
|
4
|
+
* Provides helper functions for DOM manipulation and HTML parsing
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert HTML string to DOM nodes (elements and text)
|
|
9
|
+
*
|
|
10
|
+
* This function parses an HTML string and returns an array of DOM nodes
|
|
11
|
+
* that can be used directly in WSX JSX. Text nodes are converted to strings,
|
|
12
|
+
* while HTML/SVG elements are kept as DOM elements.
|
|
13
|
+
*
|
|
14
|
+
* @param html - HTML string to parse
|
|
15
|
+
* @returns Array of HTMLElement, SVGElement, or string (for text nodes)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const nodes = parseHTMLToNodes('<p>Hello <strong>World</strong></p>');
|
|
20
|
+
* // Returns: [HTMLElement (<p>), ...]
|
|
21
|
+
*
|
|
22
|
+
* return (
|
|
23
|
+
* <div>
|
|
24
|
+
* {nodes}
|
|
25
|
+
* </div>
|
|
26
|
+
* );
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function parseHTMLToNodes(html: string): (HTMLElement | SVGElement | string)[] {
|
|
30
|
+
if (!html) return [];
|
|
31
|
+
|
|
32
|
+
// Create a temporary container to parse HTML
|
|
33
|
+
// Note: innerHTML is used here for framework-level HTML parsing utility
|
|
34
|
+
// This is an exception to the no-inner-html rule for framework code
|
|
35
|
+
const temp = document.createElement("div");
|
|
36
|
+
temp.innerHTML = html;
|
|
37
|
+
|
|
38
|
+
// Convert all child nodes to array
|
|
39
|
+
// Text nodes are converted to strings, elements are kept as-is
|
|
40
|
+
return Array.from(temp.childNodes).map((node) => {
|
|
41
|
+
if (node instanceof HTMLElement || node instanceof SVGElement) {
|
|
42
|
+
return node;
|
|
43
|
+
} else {
|
|
44
|
+
// Convert text nodes and other node types to strings
|
|
45
|
+
return node.textContent || "";
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
package/src/utils/logger.ts
CHANGED