@wsxjs/wsx-core 0.0.30 → 0.1.0

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.
@@ -19,19 +19,6 @@ interface ReactiveStateStorage {
19
19
  setter: (value: unknown | ((prev: unknown) => unknown)) => void;
20
20
  }
21
21
 
22
- /**
23
- * Focus state interface for focus preservation during rerender
24
- */
25
- interface FocusState {
26
- key: string;
27
- elementType: "input" | "textarea" | "select" | "contenteditable";
28
- value?: string;
29
- selectionStart?: number;
30
- selectionEnd?: number;
31
- scrollTop?: number; // For textarea
32
- selectedIndex?: number; // For select
33
- }
34
-
35
22
  /**
36
23
  * Base configuration interface
37
24
  */
@@ -62,28 +49,18 @@ export abstract class BaseComponent extends HTMLElement {
62
49
  protected _autoStyles?: string;
63
50
 
64
51
  /**
65
- * DOM Cache Manager for fine-grained updates (RFC 0037)
52
+ * Unique instance ID for this component
53
+ * Used for generating stable cache keys (RFC 0059)
54
+ * Can be manually set or auto-generated by cache-key.ts if missing
66
55
  * @internal
67
56
  */
68
- protected _domCache = new DOMCacheManager();
57
+ public _wsxInstanceId?: string;
69
58
 
70
59
  /**
71
- * 当前捕获的焦点状态(用于在 render 时使用捕获的值)
72
- * @internal - 由 rerender() 方法管理
73
- */
74
- protected _pendingFocusState: FocusState | null = null;
75
-
76
- /**
77
- * 防抖定时器,用于延迟重渲染(当用户正在输入时)
78
- * @internal
79
- */
80
- private _rerenderDebounceTimer: number | null = null;
81
-
82
- /**
83
- * 待处理的重渲染标志(当用户正在输入时,标记需要重渲染但延迟执行)
60
+ * DOM Cache Manager for fine-grained updates (RFC 0037)
84
61
  * @internal
85
62
  */
86
- private _pendingRerender: boolean = false;
63
+ protected _domCache = new DOMCacheManager();
87
64
 
88
65
  /**
89
66
  * 正在渲染标志(防止在 _rerender() 执行期间再次触发 scheduleRerender())
@@ -160,40 +137,6 @@ export abstract class BaseComponent extends HTMLElement {
160
137
  return this._domCache;
161
138
  }
162
139
 
163
- /**
164
- * 处理 blur 事件,在用户停止输入时执行待处理的重渲染
165
- * @internal
166
- */
167
- private handleGlobalBlur = (event: FocusEvent): void => {
168
- // 检查 blur 的元素是否在组件内
169
- const root = this.getActiveRoot();
170
- const target = event.target as HTMLElement;
171
-
172
- if (target && root.contains(target)) {
173
- // 用户停止输入,执行待处理的重渲染
174
- if (this._pendingRerender && this.connected) {
175
- // 清除防抖定时器
176
- if (this._rerenderDebounceTimer !== null) {
177
- clearTimeout(this._rerenderDebounceTimer);
178
- this._rerenderDebounceTimer = null;
179
- }
180
-
181
- // 延迟一小段时间后重渲染,确保 blur 事件完全处理
182
- requestAnimationFrame(() => {
183
- if (this._pendingRerender && this.connected && !this._isRendering) {
184
- this._pendingRerender = false;
185
- // 设置渲染标志,防止在 _rerender() 执行期间再次触发
186
- // 注意:_isRendering 标志会在 _rerender() 的 onRendered() 调用完成后清除
187
- this._isRendering = true;
188
- // 调用 _rerender() 执行实际渲染(不再调用 rerender(),避免循环)
189
- // _isRendering 标志会在 _rerender() 完成所有异步操作后清除
190
- this._rerender();
191
- }
192
- });
193
- }
194
- }
195
- };
196
-
197
140
  /**
198
141
  * 可选生命周期钩子:组件已断开
199
142
  */
@@ -260,11 +203,6 @@ export abstract class BaseComponent extends HTMLElement {
260
203
  */
261
204
  protected scheduleRerender(): void {
262
205
  if (!this.connected) {
263
- // 如果组件已断开,清除定时器
264
- if (this._rerenderDebounceTimer !== null) {
265
- clearTimeout(this._rerenderDebounceTimer);
266
- this._rerenderDebounceTimer = null;
267
- }
268
206
  return;
269
207
  }
270
208
 
@@ -279,60 +217,6 @@ export abstract class BaseComponent extends HTMLElement {
279
217
  return;
280
218
  }
281
219
 
282
- // 检查是否有需要持续输入的元素获得焦点(input、textarea、select、contenteditable)
283
- // 按钮等其他元素应该立即重渲染,以反映状态变化
284
- const root = this.getActiveRoot();
285
- let activeElement: HTMLElement | null = null;
286
-
287
- if (root instanceof ShadowRoot) {
288
- activeElement = root.activeElement as HTMLElement | null;
289
- } else {
290
- const docActiveElement = document.activeElement;
291
- if (docActiveElement && root.contains(docActiveElement)) {
292
- activeElement = docActiveElement as HTMLElement;
293
- }
294
- }
295
-
296
- // 只对需要持续输入的元素跳过重渲染(input、textarea、select、contenteditable)
297
- // 按钮等其他元素应该立即重渲染
298
- // 如果元素有 data-wsx-force-render 属性,即使是要持续输入的元素也强制重渲染
299
- if (activeElement) {
300
- // 检查是否是需要持续输入的元素
301
- // 注意:radio 和 checkbox 不需要持续输入,应该立即重渲染
302
- const isInputElement =
303
- (activeElement instanceof HTMLInputElement &&
304
- activeElement.type !== "radio" &&
305
- activeElement.type !== "checkbox") ||
306
- activeElement instanceof HTMLTextAreaElement ||
307
- activeElement instanceof HTMLSelectElement ||
308
- activeElement.hasAttribute("contenteditable");
309
-
310
- // 检查是否有强制渲染属性
311
- const forceRender = activeElement.hasAttribute("data-wsx-force-render");
312
-
313
- // 如果是输入元素且没有强制渲染属性,跳过重渲染,等待 blur 事件
314
- if (isInputElement && !forceRender) {
315
- // 标记需要重渲染,但延迟到 blur 事件
316
- this._pendingRerender = true;
317
-
318
- // 清除之前的定时器(不再使用定时器,只等待 blur)
319
- if (this._rerenderDebounceTimer !== null) {
320
- clearTimeout(this._rerenderDebounceTimer);
321
- this._rerenderDebounceTimer = null;
322
- }
323
-
324
- // 不执行重渲染,等待 blur 事件
325
- return;
326
- }
327
- // 对于按钮等其他元素,或者有 data-wsx-force-render 属性的输入元素,继续执行重渲染(不跳过)
328
- }
329
-
330
- // 没有焦点元素,立即重渲染(使用 requestAnimationFrame 确保在下一个渲染帧执行)
331
- // 如果有待处理的重渲染,也立即执行
332
- if (this._pendingRerender) {
333
- this._pendingRerender = false;
334
- }
335
-
336
220
  // 标记已调度渲染(批量更新的关键)
337
221
  this._hasScheduledRender = true;
338
222
 
@@ -383,19 +267,7 @@ export abstract class BaseComponent extends HTMLElement {
383
267
  * @internal
384
268
  */
385
269
  protected cleanup(): void {
386
- // 清除防抖定时器
387
- if (this._rerenderDebounceTimer !== null) {
388
- clearTimeout(this._rerenderDebounceTimer);
389
- this._rerenderDebounceTimer = null;
390
- }
391
-
392
- // 移除 blur 事件监听器
393
- const root = this.getActiveRoot();
394
- // 注意:在 disconnectedCallback 中,shadowRoot 仍然可用
395
- root.removeEventListener("blur", this.handleGlobalBlur as EventListener, true);
396
-
397
- // 清除待处理的重渲染标志
398
- this._pendingRerender = false;
270
+ // 清理逻辑已简化,不再需要清除定时器或事件监听器
399
271
  }
400
272
 
401
273
  /**
@@ -403,13 +275,7 @@ export abstract class BaseComponent extends HTMLElement {
403
275
  * @internal
404
276
  */
405
277
  protected initializeEventListeners(): void {
406
- // 添加 blur 事件监听器,在用户停止输入时执行待处理的重渲染
407
- // 关键修复:将监听器在这个组件的根节点上注册(ShadowRoot 或 Host Element)
408
- // 而不是 document。因为 blur 事件在 Shadow DOM 中默认是不冒泡且不跨越边界的 (composed: false)
409
- // 如果监听器在 document 上,它无法捕获到 Shadow DOM 内部元素的 blur 事件
410
- // 使用 capture: true 确保我们在事件到达目标之前捕获它(对于 blur 主要是为了捕获)
411
- const root = this.getActiveRoot();
412
- root.addEventListener("blur", this.handleGlobalBlur as EventListener, true);
278
+ // 初始化逻辑已简化,不再需要添加 blur 监听器
413
279
  }
414
280
 
415
281
  /**
@@ -491,171 +357,4 @@ export abstract class BaseComponent extends HTMLElement {
491
357
  }
492
358
  return this;
493
359
  }
494
-
495
- /**
496
- * 捕获当前焦点状态(在重渲染之前调用)
497
- * @returns 焦点状态,如果没有焦点元素则返回 null
498
- */
499
- protected captureFocusState(): FocusState | null {
500
- const root = this.getActiveRoot();
501
- let activeElement: Element | null = null;
502
-
503
- // 获取活动元素
504
- if (root instanceof ShadowRoot) {
505
- // Shadow DOM: 使用 shadowRoot.activeElement
506
- activeElement = root.activeElement;
507
- } else {
508
- // Light DOM: 检查 document.activeElement 是否在组件内
509
- const docActiveElement = document.activeElement;
510
- if (docActiveElement && root.contains(docActiveElement)) {
511
- activeElement = docActiveElement;
512
- }
513
- }
514
-
515
- if (!activeElement || !(activeElement instanceof HTMLElement)) {
516
- return null;
517
- }
518
-
519
- // 检查元素是否有 data-wsx-key 属性
520
- const key = activeElement.getAttribute("data-wsx-key");
521
- if (!key) {
522
- return null; // 元素没有 key,跳过焦点保持
523
- }
524
-
525
- const tagName = activeElement.tagName.toLowerCase();
526
- const state: FocusState = {
527
- key,
528
- elementType: tagName as FocusState["elementType"],
529
- };
530
-
531
- // 处理 input 和 textarea
532
- if (
533
- activeElement instanceof HTMLInputElement ||
534
- activeElement instanceof HTMLTextAreaElement
535
- ) {
536
- state.value = activeElement.value;
537
- state.selectionStart = activeElement.selectionStart ?? undefined;
538
- state.selectionEnd = activeElement.selectionEnd ?? undefined;
539
-
540
- // 对于 textarea,保存滚动位置
541
- if (activeElement instanceof HTMLTextAreaElement) {
542
- state.scrollTop = activeElement.scrollTop;
543
- }
544
- }
545
- // 处理 select
546
- else if (activeElement instanceof HTMLSelectElement) {
547
- state.elementType = "select";
548
- state.selectedIndex = activeElement.selectedIndex;
549
- }
550
- // 处理 contenteditable
551
- else if (activeElement.hasAttribute("contenteditable")) {
552
- state.elementType = "contenteditable";
553
- const selection = window.getSelection();
554
- if (selection && selection.rangeCount > 0) {
555
- const range = selection.getRangeAt(0);
556
- state.selectionStart = range.startOffset;
557
- state.selectionEnd = range.endOffset;
558
- }
559
- }
560
-
561
- return state;
562
- }
563
-
564
- /**
565
- * 恢复焦点状态(在重渲染之后调用)
566
- * @param state - 之前捕获的焦点状态
567
- */
568
- protected restoreFocusState(state: FocusState | null): void {
569
- if (!state || !state.key) {
570
- return;
571
- }
572
-
573
- const root = this.getActiveRoot();
574
- const target = root.querySelector(`[data-wsx-key="${state.key}"]`) as HTMLElement;
575
-
576
- if (!target) {
577
- return; // 元素未找到,跳过恢复
578
- }
579
-
580
- // 立即同步恢复值,避免闪烁
581
- // 这必须在 appendChild 之后立即执行,在浏览器渲染之前
582
- if (state.value !== undefined) {
583
- if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
584
- // 直接设置 value 属性,覆盖 render() 中设置的值
585
- // 使用 .value 而不是 setAttribute,因为 .value 是当前值,setAttribute 是初始值
586
- target.value = state.value;
587
- }
588
- }
589
-
590
- // 恢复 select 状态
591
- if (state.selectedIndex !== undefined && target instanceof HTMLSelectElement) {
592
- target.selectedIndex = state.selectedIndex;
593
- }
594
-
595
- // 使用 requestAnimationFrame 恢复焦点和光标位置
596
- // 这样可以确保 DOM 完全更新,但值已经同步恢复了
597
- requestAnimationFrame(() => {
598
- // 再次查找元素(确保元素仍然存在)
599
- const currentTarget = root.querySelector(
600
- `[data-wsx-key="${state.key}"]`
601
- ) as HTMLElement;
602
-
603
- if (!currentTarget) {
604
- return;
605
- }
606
-
607
- // 再次确保值正确(防止被其他代码覆盖)
608
- if (state.value !== undefined) {
609
- if (
610
- currentTarget instanceof HTMLInputElement ||
611
- currentTarget instanceof HTMLTextAreaElement
612
- ) {
613
- // 只有在值不同时才更新,避免触发额外的事件
614
- if (currentTarget.value !== state.value) {
615
- currentTarget.value = state.value;
616
- }
617
- }
618
- }
619
-
620
- // 聚焦元素(防止页面滚动)
621
- currentTarget.focus({ preventScroll: true });
622
-
623
- // 恢复光标/选择位置
624
- if (state.selectionStart !== undefined) {
625
- if (
626
- currentTarget instanceof HTMLInputElement ||
627
- currentTarget instanceof HTMLTextAreaElement
628
- ) {
629
- const start = state.selectionStart;
630
- const end = state.selectionEnd ?? start;
631
- currentTarget.setSelectionRange(start, end);
632
-
633
- // 恢复 textarea 滚动位置
634
- if (
635
- state.scrollTop !== undefined &&
636
- currentTarget instanceof HTMLTextAreaElement
637
- ) {
638
- currentTarget.scrollTop = state.scrollTop;
639
- }
640
- } else if (currentTarget.hasAttribute("contenteditable")) {
641
- // 恢复 contenteditable 选择
642
- const selection = window.getSelection();
643
- if (selection) {
644
- const range = document.createRange();
645
- const textNode = currentTarget.childNodes[0];
646
- if (textNode && textNode.nodeType === Node.TEXT_NODE) {
647
- const maxPos = Math.min(
648
- state.selectionStart,
649
- textNode.textContent?.length || 0
650
- );
651
- range.setStart(textNode, maxPos);
652
- range.setEnd(textNode, state.selectionEnd ?? maxPos);
653
- selection.removeAllRanges();
654
- selection.addRange(range);
655
- }
656
- }
657
- }
658
- }
659
- });
660
- }
661
360
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  // Core exports
2
+ export * from "./base-component";
2
3
  export { WebComponent } from "./web-component";
3
4
  export { LightComponent } from "./light-component";
4
5
  export { autoRegister, registerComponent } from "./auto-register";
@@ -201,12 +201,14 @@ export function Fragment(_props: unknown, children: JSXChildren[]): DocumentFrag
201
201
 
202
202
  if (typeof child === "string" || typeof child === "number") {
203
203
  const textNode = document.createTextNode(String(child));
204
- (textNode as any).__wsxManaged = true;
204
+ (textNode as Text & { __wsxManaged?: boolean }).__wsxManaged = true;
205
205
  fragment.appendChild(textNode);
206
206
  } else if (child instanceof HTMLElement || child instanceof SVGElement) {
207
207
  fragment.appendChild(child);
208
208
  } else if (child instanceof DocumentFragment) {
209
209
  fragment.appendChild(child);
210
+ } else if (child instanceof Node) {
211
+ fragment.appendChild(child);
210
212
  }
211
213
  });
212
214