@wsxjs/wsx-core 0.0.20 → 0.0.22

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,307 @@
1
+ /**
2
+ * Props Utilities
3
+ *
4
+ * Pure functions for handling element properties and attributes (RFC 0037).
5
+ * These functions implement the HTML First strategy for property setting.
6
+ */
7
+
8
+ import { getSVGAttributeName, shouldUseSVGNamespace } from "./svg-utils";
9
+ import { createLogger } from "./logger";
10
+ const logger = createLogger("Props Utilities");
11
+
12
+ /**
13
+ * 检查是否是框架内部属性
14
+ * 这些属性不应该被渲染到 DOM 元素上
15
+ *
16
+ * @param key - 属性名
17
+ * @returns 是否是框架内部属性
18
+ */
19
+ export function isFrameworkInternalProp(key: string): boolean {
20
+ // JSX 标准:key 不应该渲染到 DOM
21
+ if (key === "key") {
22
+ return true;
23
+ }
24
+
25
+ // 框架内部属性(用于缓存和优化)
26
+ if (key === "__wsxPositionId" || key === "__wsxIndex") {
27
+ return true;
28
+ }
29
+
30
+ // 测试辅助属性(不应该渲染到 DOM)
31
+ if (key === "__testId") {
32
+ return true;
33
+ }
34
+
35
+ // ref 已经在 applySingleProp 中处理,但这里也标记为内部属性
36
+ // 确保不会传递到 setSmartProperty
37
+ if (key === "ref") {
38
+ return true;
39
+ }
40
+
41
+ return false;
42
+ }
43
+
44
+ /**
45
+ * 检查是否是 HTML 标准属性
46
+ * HTML First 策略的核心:优先识别标准属性
47
+ *
48
+ * @param key - 属性名
49
+ * @returns 是否是标准 HTML 属性
50
+ */
51
+ export function isStandardHTMLAttribute(key: string): boolean {
52
+ // 标准 HTML 属性集合(常见属性)
53
+ const standardAttributes = new Set([
54
+ // 全局属性
55
+ "id",
56
+ "class",
57
+ "className",
58
+ "style",
59
+ "title",
60
+ "lang",
61
+ "dir",
62
+ "hidden",
63
+ "tabindex",
64
+ "accesskey",
65
+ "contenteditable",
66
+ "draggable",
67
+ "spellcheck",
68
+ "translate",
69
+ "autocapitalize",
70
+ "autocorrect",
71
+ // 表单属性
72
+ "name",
73
+ "value",
74
+ "type",
75
+ "placeholder",
76
+ "required",
77
+ "disabled",
78
+ "readonly",
79
+ "checked",
80
+ "selected",
81
+ "multiple",
82
+ "min",
83
+ "max",
84
+ "step",
85
+ "autocomplete",
86
+ "autofocus",
87
+ "form",
88
+ "formaction",
89
+ "formenctype",
90
+ "formmethod",
91
+ "formnovalidate",
92
+ "formtarget",
93
+ // 链接属性
94
+ "href",
95
+ "target",
96
+ "rel",
97
+ "download",
98
+ "hreflang",
99
+ "ping",
100
+ // 媒体属性
101
+ "src",
102
+ "alt",
103
+ "width",
104
+ "height",
105
+ "poster",
106
+ "preload",
107
+ "controls",
108
+ "autoplay",
109
+ "loop",
110
+ "muted",
111
+ "playsinline",
112
+ "crossorigin",
113
+ // ARIA 属性(部分常见)
114
+ "role",
115
+ ]);
116
+
117
+ const lowerKey = key.toLowerCase();
118
+
119
+ // 检查是否是标准属性
120
+ if (standardAttributes.has(lowerKey)) {
121
+ return true;
122
+ }
123
+
124
+ // 检查是否是 data-* 属性(必须使用连字符)
125
+ // 注意:单独的 "data" 不是标准属性,不在这个列表中
126
+ // data 可以检查 JavaScript 属性,data-* 只使用 setAttribute
127
+ if (lowerKey.startsWith("data-")) {
128
+ return true; // 标准属性,只使用 setAttribute,不检查对象属性
129
+ }
130
+
131
+ // 检查是否是 aria-* 属性
132
+ if (lowerKey.startsWith("aria-")) {
133
+ return true;
134
+ }
135
+
136
+ // 检查是否是 SVG 命名空间属性
137
+ if (key.startsWith("xml:") || key.startsWith("xlink:")) {
138
+ return true;
139
+ }
140
+
141
+ return false;
142
+ }
143
+
144
+ /**
145
+ * 检查是否是特殊属性(已有专门处理逻辑的属性)
146
+ * 这些属性不应该进入通用属性处理流程
147
+ */
148
+ export function isSpecialProperty(key: string, value: unknown): boolean {
149
+ return (
150
+ key === "ref" ||
151
+ key === "className" ||
152
+ key === "class" ||
153
+ key === "style" ||
154
+ (key.startsWith("on") && typeof value === "function") ||
155
+ typeof value === "boolean" ||
156
+ key === "value"
157
+ );
158
+ }
159
+
160
+ /**
161
+ * 智能属性设置函数
162
+ * HTML First 策略:优先使用 HTML 属性,避免与标准属性冲突
163
+ *
164
+ * @param element - DOM 元素
165
+ * @param key - 属性名
166
+ * @param value - 属性值
167
+ * @param tag - HTML 标签名(用于判断是否是 SVG)
168
+ */
169
+ export function setSmartProperty(
170
+ element: HTMLElement | SVGElement,
171
+ key: string,
172
+ value: unknown,
173
+ tag: string
174
+ ): void {
175
+ const isSVG = shouldUseSVGNamespace(tag);
176
+
177
+ // 1. 检查是否是特殊属性(已有处理逻辑的属性)
178
+ if (isSpecialProperty(key, value)) {
179
+ return; // 由现有逻辑处理
180
+ }
181
+
182
+ // 2. HTML First: 优先检查是否是 HTML 标准属性
183
+ if (isStandardHTMLAttribute(key)) {
184
+ // 标准 HTML 属性:直接使用 setAttribute,不检查 JavaScript 属性
185
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
186
+
187
+ // 对于复杂类型,尝试序列化
188
+ if (typeof value === "object" && value !== null) {
189
+ try {
190
+ const serialized = JSON.stringify(value);
191
+ // 检查长度限制(保守估计 1MB)
192
+ if (serialized.length > 1024 * 1024) {
193
+ logger.warn(
194
+ `[WSX] Attribute "${key}" value too large, ` +
195
+ `consider using a non-standard property name instead`
196
+ );
197
+ }
198
+ element.setAttribute(attributeName, serialized);
199
+ } catch (error) {
200
+ // 无法序列化(如循环引用),警告并跳过
201
+ logger.warn(`Cannot serialize attribute "${key}":`, error);
202
+ }
203
+ } else {
204
+ element.setAttribute(attributeName, String(value));
205
+ }
206
+ // 重要:标准属性只使用 setAttribute,不使用 JavaScript 属性
207
+ return;
208
+ }
209
+
210
+ // 3. SVG 元素特殊处理:对于 SVG 元素,很多属性应该直接使用 setAttribute
211
+ // 因为 SVG 元素的很多属性是只读的(如 viewBox)
212
+ if (element instanceof SVGElement) {
213
+ const attributeName = getSVGAttributeName(key);
214
+ // 对于复杂类型,尝试序列化
215
+ if (typeof value === "object" && value !== null) {
216
+ try {
217
+ const serialized = JSON.stringify(value);
218
+ element.setAttribute(attributeName, serialized);
219
+ } catch (error) {
220
+ logger.warn(`Cannot serialize SVG attribute "${key}":`, error);
221
+ }
222
+ } else {
223
+ element.setAttribute(attributeName, String(value));
224
+ }
225
+ return;
226
+ }
227
+
228
+ // 4. 非标准属性:检查元素是否有该 JavaScript 属性
229
+ const hasProperty = key in element || Object.prototype.hasOwnProperty.call(element, key);
230
+
231
+ if (hasProperty) {
232
+ // 检查是否是只读属性
233
+ let isReadOnly = false;
234
+ try {
235
+ const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), key);
236
+ if (descriptor) {
237
+ isReadOnly =
238
+ (descriptor.get !== undefined && descriptor.set === undefined) ||
239
+ (descriptor.writable === false && descriptor.set === undefined);
240
+ }
241
+ } catch {
242
+ // 忽略错误,继续尝试设置
243
+ }
244
+
245
+ if (isReadOnly) {
246
+ // 只读属性使用 setAttribute
247
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
248
+ // 对于复杂类型,尝试序列化
249
+ if (typeof value === "object" && value !== null) {
250
+ try {
251
+ const serialized = JSON.stringify(value);
252
+ element.setAttribute(attributeName, serialized);
253
+ } catch (error) {
254
+ logger.warn(`Cannot serialize readonly property "${key}":`, error);
255
+ }
256
+ } else {
257
+ element.setAttribute(attributeName, String(value));
258
+ }
259
+ } else {
260
+ // 使用 JavaScript 属性赋值(支持任意类型)
261
+ try {
262
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
263
+ (element as any)[key] = value;
264
+ } catch {
265
+ // 如果赋值失败,回退到 setAttribute
266
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
267
+ // 对于复杂类型,尝试序列化
268
+ if (typeof value === "object" && value !== null) {
269
+ try {
270
+ const serialized = JSON.stringify(value);
271
+ element.setAttribute(attributeName, serialized);
272
+ } catch (error) {
273
+ logger.warn(
274
+ `[WSX] Cannot serialize property "${key}" for attribute:`,
275
+ error
276
+ );
277
+ }
278
+ } else {
279
+ element.setAttribute(attributeName, String(value));
280
+ }
281
+ }
282
+ }
283
+ } else {
284
+ // 没有 JavaScript 属性,使用 setAttribute
285
+ const attributeName = isSVG ? getSVGAttributeName(key) : key;
286
+
287
+ // 对于复杂类型,尝试序列化
288
+ if (typeof value === "object" && value !== null) {
289
+ try {
290
+ const serialized = JSON.stringify(value);
291
+ // 检查长度限制
292
+ if (serialized.length > 1024 * 1024) {
293
+ logger.warn(
294
+ `[WSX] Property "${key}" value too large for attribute, ` +
295
+ `consider using a JavaScript property instead`
296
+ );
297
+ }
298
+ element.setAttribute(attributeName, serialized);
299
+ } catch (error) {
300
+ // 无法序列化,警告并跳过
301
+ logger.warn(`Cannot serialize property "${key}" for attribute:`, error);
302
+ }
303
+ } else {
304
+ element.setAttribute(attributeName, String(value));
305
+ }
306
+ }
307
+ }
@@ -11,6 +11,8 @@
11
11
  import { h, type JSXChildren } from "./jsx-factory";
12
12
  import { StyleManager } from "./styles/style-manager";
13
13
  import { BaseComponent, type BaseComponentConfig } from "./base-component";
14
+ import { RenderContext } from "./render-context";
15
+ import { shouldPreserveElement } from "./utils/element-marking";
14
16
  import { createLogger } from "@wsxjs/wsx-logger";
15
17
 
16
18
  const logger = createLogger("WebComponent");
@@ -99,7 +101,9 @@ export abstract class WebComponent extends BaseComponent {
99
101
  // 渲染JSX内容到Shadow DOM
100
102
  // render() 应该返回包含 slot 元素的内容(如果需要)
101
103
  // 浏览器会自动将 Light DOM 中的内容分配到 slot
102
- const content = this.render();
104
+ // 关键修复:使用 RenderContext.runInContext() 确保 h() 能够获取上下文
105
+ // 否则,首次渲染时创建的元素不会被标记 __wsxCacheKey,导致重复元素问题
106
+ const content = RenderContext.runInContext(this, () => this.render());
103
107
  this.shadowRoot.appendChild(content);
104
108
  }
105
109
 
@@ -183,7 +187,7 @@ export abstract class WebComponent extends BaseComponent {
183
187
  }
184
188
 
185
189
  // 4. 重新渲染JSX
186
- const content = this.render();
190
+ const content = RenderContext.runInContext(this, () => this.render());
187
191
 
188
192
  // 5. 在添加到 DOM 之前恢复值
189
193
  if (focusState && focusState.key && focusState.value !== undefined) {
@@ -211,10 +215,24 @@ export abstract class WebComponent extends BaseComponent {
211
215
  // 添加新内容
212
216
  this.shadowRoot.appendChild(content);
213
217
 
214
- // 移除旧内容
215
- const oldChildren = Array.from(this.shadowRoot.children).filter(
216
- (child) => child !== content
217
- );
218
+ // 移除旧内容(保留样式元素和未标记元素)
219
+ // 关键修复:使用 shouldPreserveElement() 来保护第三方库注入的元素
220
+ const oldChildren = Array.from(this.shadowRoot.children).filter((child) => {
221
+ // 保留新添加的内容
222
+ if (child === content) {
223
+ return false;
224
+ }
225
+ // 保留样式元素
226
+ if (child instanceof HTMLStyleElement) {
227
+ return false;
228
+ }
229
+ // 保留未标记的元素(第三方库注入的元素、自定义元素)
230
+ // 这是 RFC 0037 Phase 5 的核心:保护未标记元素
231
+ if (shouldPreserveElement(child)) {
232
+ return false;
233
+ }
234
+ return true;
235
+ });
218
236
  oldChildren.forEach((child) => child.remove());
219
237
 
220
238
  // 恢复焦点状态