what-core 0.8.3 → 0.10.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.
- package/dist/chunk-AW3BAPIK.js +1685 -0
- package/dist/chunk-AW3BAPIK.js.map +7 -0
- package/dist/chunk-AZP2EOGX.js +188 -0
- package/dist/chunk-AZP2EOGX.js.map +7 -0
- package/dist/chunk-F2HUXI22.js +1675 -0
- package/dist/chunk-F2HUXI22.js.map +7 -0
- package/dist/chunk-KBM6CWG4.min.js +2 -0
- package/dist/chunk-KBM6CWG4.min.js.map +7 -0
- package/dist/chunk-KL7TNUIU.min.js +2 -0
- package/dist/chunk-KL7TNUIU.min.js.map +7 -0
- package/dist/chunk-L6XOF7P4.min.js +2 -0
- package/dist/chunk-L6XOF7P4.min.js.map +7 -0
- package/dist/chunk-M7UEET5O.js +1323 -0
- package/dist/chunk-M7UEET5O.js.map +7 -0
- package/dist/chunk-O3SKPRTY.min.js +2 -0
- package/dist/chunk-O3SKPRTY.min.js.map +7 -0
- package/dist/chunk-RN6QIBWL.min.js +2 -0
- package/dist/chunk-RN6QIBWL.min.js.map +7 -0
- package/dist/chunk-VMTTYB4L.min.js +2 -0
- package/dist/chunk-VMTTYB4L.min.js.map +7 -0
- package/dist/chunk-VP4WLF5A.js +1323 -0
- package/dist/chunk-VP4WLF5A.js.map +7 -0
- package/dist/chunk-YA3W4XKH.js +1323 -0
- package/dist/chunk-YA3W4XKH.js.map +7 -0
- package/dist/index.js +212 -2785
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +6 -6
- package/dist/index.min.js.map +4 -4
- package/dist/jsx-dev-runtime.js +4 -53
- package/dist/jsx-dev-runtime.js.map +3 -3
- package/dist/jsx-dev-runtime.min.js +1 -1
- package/dist/jsx-dev-runtime.min.js.map +4 -4
- package/dist/jsx-runtime.js +4 -53
- package/dist/jsx-runtime.js.map +3 -3
- package/dist/jsx-runtime.min.js +1 -1
- package/dist/jsx-runtime.min.js.map +4 -4
- package/dist/render.js +22 -2044
- package/dist/render.js.map +4 -4
- package/dist/render.min.js +1 -1
- package/dist/render.min.js.map +4 -4
- package/dist/testing.js +13 -1079
- package/dist/testing.js.map +4 -4
- package/dist/testing.min.js +1 -1
- package/dist/testing.min.js.map +4 -4
- package/package.json +2 -2
- package/src/dom.js +54 -6
- package/src/h.js +15 -3
- package/src/head.js +72 -2
- package/src/hooks.js +65 -4
- package/src/hydration-data.js +34 -0
- package/src/index.js +9 -2
- package/src/reactive.js +78 -1
- package/src/render.js +450 -105
- package/src/server-context.js +48 -0
- package/src/store.js +6 -2
package/src/render.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// No VDOM diffing — direct DOM manipulation with surgical signal-driven updates.
|
|
4
4
|
|
|
5
5
|
import { effect, untrack, createRoot, _createItemScope, signal, __DEV__ } from './reactive.js';
|
|
6
|
-
import { createDOM, disposeTree, getCurrentComponent, getComponentStack } from './dom.js';
|
|
6
|
+
import { createDOM, disposeTree, getCurrentComponent, getComponentStack, _setSelectValue } from './dom.js';
|
|
7
7
|
export { effect, untrack };
|
|
8
8
|
|
|
9
9
|
// --- Generic text insertion hook ---
|
|
@@ -167,6 +167,11 @@ export function svgTemplate(html) {
|
|
|
167
167
|
// - array → insert each element
|
|
168
168
|
|
|
169
169
|
export function insert(parent, child, marker) {
|
|
170
|
+
// mapArray inserter: self-managing reactive list with its own effect
|
|
171
|
+
if (typeof child === 'function' && child._mapArray) {
|
|
172
|
+
return child(parent, marker || null);
|
|
173
|
+
}
|
|
174
|
+
|
|
170
175
|
if (typeof child === 'function') {
|
|
171
176
|
// Fast path: if the first evaluation returns a string/number, optimistically
|
|
172
177
|
// create a text node for direct updates. If the value type changes later
|
|
@@ -197,8 +202,10 @@ export function insert(parent, child, marker) {
|
|
|
197
202
|
});
|
|
198
203
|
return textNode;
|
|
199
204
|
}
|
|
200
|
-
// General path for non-text reactive children (first value was null/vnode/array)
|
|
201
|
-
|
|
205
|
+
// General path for non-text reactive children (first value was null/vnode/array).
|
|
206
|
+
// Let the effect handle both the initial insert and subsequent updates to avoid
|
|
207
|
+
// double-evaluating child() (which would create components twice on mount).
|
|
208
|
+
let current = null;
|
|
202
209
|
effect(() => {
|
|
203
210
|
current = reconcileInsert(parent, child(), current, marker || null);
|
|
204
211
|
});
|
|
@@ -256,13 +263,28 @@ function valuesToNodes(value, parent, out) {
|
|
|
256
263
|
return out;
|
|
257
264
|
}
|
|
258
265
|
|
|
266
|
+
// Resolve function values (reactive accessors passed through props)
|
|
267
|
+
if (typeof value === 'function') {
|
|
268
|
+
valuesToNodes(value(), parent, out);
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
|
|
259
272
|
if (typeof value === 'string' || typeof value === 'number') {
|
|
260
273
|
out.push(document.createTextNode(String(value)));
|
|
261
274
|
return out;
|
|
262
275
|
}
|
|
263
276
|
|
|
264
277
|
if (isDomNode(value)) {
|
|
265
|
-
|
|
278
|
+
// DocumentFragments lose their children on DOM insertion, making them
|
|
279
|
+
// untrackable for reconciliation. Flatten to child nodes instead.
|
|
280
|
+
if (value.nodeType === 11 && value.childNodes.length > 0) {
|
|
281
|
+
const children = Array.from(value.childNodes);
|
|
282
|
+
for (let i = 0; i < children.length; i++) {
|
|
283
|
+
out.push(children[i]);
|
|
284
|
+
}
|
|
285
|
+
} else {
|
|
286
|
+
out.push(value);
|
|
287
|
+
}
|
|
266
288
|
return out;
|
|
267
289
|
}
|
|
268
290
|
|
|
@@ -383,7 +405,7 @@ export function mapArray(source, mapFn, options) {
|
|
|
383
405
|
const keyFn = options?.key;
|
|
384
406
|
const raw = options?.raw || false;
|
|
385
407
|
|
|
386
|
-
|
|
408
|
+
const inserter = (parent, marker) => {
|
|
387
409
|
let items = [];
|
|
388
410
|
let mappedNodes = [];
|
|
389
411
|
let disposeFns = [];
|
|
@@ -407,6 +429,8 @@ export function mapArray(source, mapFn, options) {
|
|
|
407
429
|
|
|
408
430
|
return endMarker;
|
|
409
431
|
};
|
|
432
|
+
inserter._mapArray = true;
|
|
433
|
+
return inserter;
|
|
410
434
|
}
|
|
411
435
|
|
|
412
436
|
function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, disposeFns, mapFn) {
|
|
@@ -424,10 +448,10 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
|
|
|
424
448
|
for (let i = oldLen - 1; i >= 0; i--) {
|
|
425
449
|
const node = mappedNodes[i];
|
|
426
450
|
if (node) {
|
|
427
|
-
//
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
451
|
+
// disposeTree walks the subtree for nested component contexts
|
|
452
|
+
// (c:start comments) and reactive bindings that the item-scope
|
|
453
|
+
// dispose above does not cover. (AUDIT C5)
|
|
454
|
+
disposeTree(node);
|
|
431
455
|
if (node.parentNode === parent) parent.removeChild(node);
|
|
432
456
|
}
|
|
433
457
|
}
|
|
@@ -490,6 +514,7 @@ function reconcileList(parent, endMarker, oldItems, newItems, mappedNodes, dispo
|
|
|
490
514
|
// Only removals in the middle
|
|
491
515
|
for (let i = start; i <= oldEnd; i++) {
|
|
492
516
|
disposeFns[i]?.();
|
|
517
|
+
if (mappedNodes[i]) disposeTree(mappedNodes[i]); // dispose nested component ctx (AUDIT C5)
|
|
493
518
|
if (mappedNodes[i]?.parentNode) mappedNodes[i].parentNode.removeChild(mappedNodes[i]);
|
|
494
519
|
}
|
|
495
520
|
} else if (midOldLen === 0) {
|
|
@@ -547,6 +572,7 @@ function _reconcileMiddle(parent, endMarker, oldItems, newItems, mappedNodes, di
|
|
|
547
572
|
// Dispose removed items
|
|
548
573
|
for (const [, oldIdx] of oldIdxMap) {
|
|
549
574
|
disposeFns[oldIdx]?.();
|
|
575
|
+
if (mappedNodes[oldIdx]) disposeTree(mappedNodes[oldIdx]); // dispose nested component ctx (AUDIT C5)
|
|
550
576
|
if (mappedNodes[oldIdx]?.parentNode) mappedNodes[oldIdx].parentNode.removeChild(mappedNodes[oldIdx]);
|
|
551
577
|
}
|
|
552
578
|
|
|
@@ -655,6 +681,73 @@ function _lis(arr, len) {
|
|
|
655
681
|
// When a key persists but its item reference changes, the item signal updates
|
|
656
682
|
// in place — no DOM node destruction/creation. Only effects reading the
|
|
657
683
|
// item accessor re-run (e.g., textContent update for changed label).
|
|
684
|
+
//
|
|
685
|
+
// Multi-node items: Components return DocumentFragments (c:start, content, c:end).
|
|
686
|
+
// We track each item via a start-marker comment. Moving/removing an item moves
|
|
687
|
+
// all nodes from its marker up to (but not including) the next item's marker.
|
|
688
|
+
|
|
689
|
+
function _createItemMarker() {
|
|
690
|
+
return document.createComment('i');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Collect all DOM nodes belonging to one item (from its marker to beforeEnd).
|
|
694
|
+
function _collectItemNodes(marker, beforeEnd) {
|
|
695
|
+
const nodes = [];
|
|
696
|
+
let n = marker;
|
|
697
|
+
while (n && n !== beforeEnd) {
|
|
698
|
+
nodes.push(n);
|
|
699
|
+
n = n.nextSibling;
|
|
700
|
+
}
|
|
701
|
+
return nodes;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Move all nodes for an item (starting at marker) before `ref` in `parent`.
|
|
705
|
+
function _moveItem(parent, marker, beforeEnd, ref) {
|
|
706
|
+
let n = marker;
|
|
707
|
+
while (n && n !== beforeEnd) {
|
|
708
|
+
const next = n.nextSibling;
|
|
709
|
+
parent.insertBefore(n, ref);
|
|
710
|
+
n = next;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Remove all nodes for an item from the DOM.
|
|
715
|
+
function _removeItemNodes(parent, marker, beforeEnd) {
|
|
716
|
+
let n = marker;
|
|
717
|
+
while (n && n !== beforeEnd) {
|
|
718
|
+
const next = n.nextSibling;
|
|
719
|
+
// Always disposeTree: a component's context lives on its `c:start` comment
|
|
720
|
+
// (nodeType 8, via _commentCtxMap) which carries none of the gate flags
|
|
721
|
+
// below, so the old `_componentCtx || _dispose || _propEffects` guard
|
|
722
|
+
// leaked every component's effects/cleanups/onCleanup/listeners on removal.
|
|
723
|
+
// disposeTree is internally cheap-guarded and idempotent. (AUDIT C5)
|
|
724
|
+
disposeTree(n);
|
|
725
|
+
parent.removeChild(n);
|
|
726
|
+
n = next;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Create a new item: wraps mapFn result in a marker + appends to target.
|
|
731
|
+
function _createKeyedItem(target, item, idx, keyFn, keyedState, mapFn, mappedArr, disposeArr, signal_) {
|
|
732
|
+
let accessor;
|
|
733
|
+
if (keyedState) {
|
|
734
|
+
const key = keyFn(item);
|
|
735
|
+
const itemSig = signal_(item);
|
|
736
|
+
accessor = itemSig;
|
|
737
|
+
keyedState.set(key, { itemSig });
|
|
738
|
+
} else {
|
|
739
|
+
accessor = item;
|
|
740
|
+
}
|
|
741
|
+
const marker = _createItemMarker();
|
|
742
|
+
target.appendChild(marker);
|
|
743
|
+
const result = _createItemScope(dispose => {
|
|
744
|
+
disposeArr[idx] = dispose;
|
|
745
|
+
return mapFn(accessor, idx);
|
|
746
|
+
});
|
|
747
|
+
// result may be a DocumentFragment or a single node
|
|
748
|
+
target.appendChild(result);
|
|
749
|
+
mappedArr[idx] = marker;
|
|
750
|
+
}
|
|
658
751
|
|
|
659
752
|
function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disposeFns, mapFn, keyFn, keyedState) {
|
|
660
753
|
const newLen = newItems.length;
|
|
@@ -663,18 +756,12 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
663
756
|
// --- Fast path: clear all ---
|
|
664
757
|
if (newLen === 0) {
|
|
665
758
|
if (oldLen > 0) {
|
|
666
|
-
// Dispose reactive scopes first, then remove DOM nodes.
|
|
667
759
|
for (let i = 0; i < oldLen; i++) {
|
|
668
760
|
if (disposeFns[i]) disposeFns[i]();
|
|
669
761
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (node._componentCtx || node._dispose || node._propEffects) {
|
|
674
|
-
disposeTree(node);
|
|
675
|
-
}
|
|
676
|
-
if (node.parentNode === parent) parent.removeChild(node);
|
|
677
|
-
}
|
|
762
|
+
// Remove all nodes between first item marker and endMarker
|
|
763
|
+
if (mappedNodes[0]) {
|
|
764
|
+
_removeItemNodes(parent, mappedNodes[0], endMarker);
|
|
678
765
|
}
|
|
679
766
|
mappedNodes.length = 0;
|
|
680
767
|
disposeFns.length = 0;
|
|
@@ -687,23 +774,7 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
687
774
|
if (oldLen === 0) {
|
|
688
775
|
const frag = document.createDocumentFragment();
|
|
689
776
|
for (let i = 0; i < newLen; i++) {
|
|
690
|
-
|
|
691
|
-
const idx = i;
|
|
692
|
-
let accessor;
|
|
693
|
-
if (keyedState) {
|
|
694
|
-
const key = keyFn(item);
|
|
695
|
-
const itemSig = signal(item);
|
|
696
|
-
accessor = itemSig;
|
|
697
|
-
keyedState.set(key, { itemSig });
|
|
698
|
-
} else {
|
|
699
|
-
accessor = item; // raw mode: pass item directly
|
|
700
|
-
}
|
|
701
|
-
const node = _createItemScope(dispose => {
|
|
702
|
-
disposeFns[idx] = dispose;
|
|
703
|
-
return mapFn(accessor, idx);
|
|
704
|
-
});
|
|
705
|
-
mappedNodes[i] = node;
|
|
706
|
-
frag.appendChild(node);
|
|
777
|
+
_createKeyedItem(frag, newItems[i], i, keyFn, keyedState, mapFn, mappedNodes, disposeFns, signal);
|
|
707
778
|
}
|
|
708
779
|
parent.insertBefore(frag, endMarker);
|
|
709
780
|
return;
|
|
@@ -713,12 +784,10 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
713
784
|
let start = 0;
|
|
714
785
|
const minLen = Math.min(oldLen, newLen);
|
|
715
786
|
while (start < minLen) {
|
|
716
|
-
// Fast path: same reference → same key, no update needed
|
|
717
787
|
if (oldItems[start] === newItems[start]) { start++; continue; }
|
|
718
788
|
const oldKey = keyFn(oldItems[start]);
|
|
719
789
|
const newKey = keyFn(newItems[start]);
|
|
720
790
|
if (oldKey !== newKey) break;
|
|
721
|
-
// Key matches but reference changed — update signal (non-raw mode only)
|
|
722
791
|
if (keyedState) keyedState.get(oldKey).itemSig.set(newItems[start]);
|
|
723
792
|
start++;
|
|
724
793
|
}
|
|
@@ -736,13 +805,8 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
736
805
|
newEnd--;
|
|
737
806
|
}
|
|
738
807
|
|
|
739
|
-
|
|
740
|
-
if (start > oldEnd && start > newEnd) {
|
|
741
|
-
// Just copy existing mappings to output
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
808
|
+
if (start > oldEnd && start > newEnd) return;
|
|
744
809
|
|
|
745
|
-
// Copy prefix/suffix into output arrays
|
|
746
810
|
const newMapped = new Array(newLen);
|
|
747
811
|
const newDispose = new Array(newLen);
|
|
748
812
|
for (let i = 0; i < start; i++) {
|
|
@@ -760,27 +824,12 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
760
824
|
|
|
761
825
|
// --- Only additions in middle ---
|
|
762
826
|
if (midOldLen === 0) {
|
|
763
|
-
const
|
|
827
|
+
const ref = newEnd + 1 < newLen && newMapped[newEnd + 1] ? newMapped[newEnd + 1] : endMarker;
|
|
764
828
|
const frag = document.createDocumentFragment();
|
|
765
829
|
for (let i = start; i <= newEnd; i++) {
|
|
766
|
-
|
|
767
|
-
const idx = i;
|
|
768
|
-
let accessor;
|
|
769
|
-
if (keyedState) {
|
|
770
|
-
const key = keyFn(item);
|
|
771
|
-
const itemSig = signal(item);
|
|
772
|
-
accessor = itemSig;
|
|
773
|
-
keyedState.set(key, { itemSig });
|
|
774
|
-
} else {
|
|
775
|
-
accessor = item;
|
|
776
|
-
}
|
|
777
|
-
newMapped[i] = _createItemScope(dispose => {
|
|
778
|
-
newDispose[idx] = dispose;
|
|
779
|
-
return mapFn(accessor, idx);
|
|
780
|
-
});
|
|
781
|
-
frag.appendChild(newMapped[i]);
|
|
830
|
+
_createKeyedItem(frag, newItems[i], i, keyFn, keyedState, mapFn, newMapped, newDispose, signal);
|
|
782
831
|
}
|
|
783
|
-
parent.insertBefore(frag,
|
|
832
|
+
parent.insertBefore(frag, ref);
|
|
784
833
|
_copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen);
|
|
785
834
|
return;
|
|
786
835
|
}
|
|
@@ -789,15 +838,243 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
789
838
|
if (midNewLen === 0) {
|
|
790
839
|
for (let i = start; i <= oldEnd; i++) {
|
|
791
840
|
disposeFns[i]?.();
|
|
792
|
-
|
|
841
|
+
// Compute the range boundary from the live DOM. Sibling markers in
|
|
842
|
+
// mappedNodes may have been detached by earlier iterations of this loop;
|
|
843
|
+
// walking the DOM finds the next surviving item marker (or endMarker).
|
|
844
|
+
const rangeEnd = _findNextMarkerAfter(parent, mappedNodes[i], mappedNodes, i, endMarker);
|
|
845
|
+
_removeItemNodes(parent, mappedNodes[i], rangeEnd);
|
|
793
846
|
if (keyedState) keyedState.delete(keyFn(oldItems[i]));
|
|
794
847
|
}
|
|
795
848
|
_copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen);
|
|
796
849
|
return;
|
|
797
850
|
}
|
|
798
851
|
|
|
852
|
+
// --- Fast paths for common small-move cases ---
|
|
853
|
+
// Detect swap (2 mismatches) or single-move (contiguous shift) cheaply
|
|
854
|
+
// before falling through to the expensive LIS + backward-walk general case.
|
|
855
|
+
|
|
856
|
+
if (midNewLen === midOldLen && midNewLen >= 2 && midNewLen <= Math.max(midOldLen, 200)) {
|
|
857
|
+
// Count positions where keys differ
|
|
858
|
+
let mismatchCount = 0;
|
|
859
|
+
let mm1 = -1, mm2 = -1; // first two mismatch indices (relative to start)
|
|
860
|
+
for (let i = 0; i < midNewLen && mismatchCount <= 4; i++) {
|
|
861
|
+
const oldKey = keyFn(oldItems[start + i]);
|
|
862
|
+
const newKey = keyFn(newItems[start + i]);
|
|
863
|
+
if (oldKey !== newKey) {
|
|
864
|
+
if (mismatchCount === 0) mm1 = i;
|
|
865
|
+
else if (mismatchCount === 1) mm2 = i;
|
|
866
|
+
mismatchCount++;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// --- Fast path A: Pure swap (exactly 2 key mismatches, keys exchanged) ---
|
|
871
|
+
if (mismatchCount === 2) {
|
|
872
|
+
const i1 = start + mm1, i2 = start + mm2;
|
|
873
|
+
const oldKey1 = keyFn(oldItems[i1]), oldKey2 = keyFn(oldItems[i2]);
|
|
874
|
+
const newKey1 = keyFn(newItems[i1]), newKey2 = keyFn(newItems[i2]);
|
|
875
|
+
|
|
876
|
+
if (oldKey1 === newKey2 && oldKey2 === newKey1) {
|
|
877
|
+
// Confirmed swap. Move item at i2's DOM position before item at i1's position,
|
|
878
|
+
// then move i1's nodes to where i2 was.
|
|
879
|
+
for (let i = 0; i < start; i++) {
|
|
880
|
+
newMapped[i] = mappedNodes[i];
|
|
881
|
+
newDispose[i] = disposeFns[i];
|
|
882
|
+
}
|
|
883
|
+
for (let i = start; i <= newEnd; i++) {
|
|
884
|
+
newMapped[i] = mappedNodes[i];
|
|
885
|
+
newDispose[i] = disposeFns[i];
|
|
886
|
+
}
|
|
887
|
+
for (let i = newEnd + 1; i < newLen; i++) {
|
|
888
|
+
const oldI = oldEnd + 1 + (i - newEnd - 1);
|
|
889
|
+
newMapped[i] = mappedNodes[oldI];
|
|
890
|
+
newDispose[i] = disposeFns[oldI];
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Swap mapped entries
|
|
894
|
+
const tmpM = newMapped[i1]; newMapped[i1] = newMapped[i2]; newMapped[i2] = tmpM;
|
|
895
|
+
const tmpD = newDispose[i1]; newDispose[i1] = newDispose[i2]; newDispose[i2] = tmpD;
|
|
896
|
+
|
|
897
|
+
// Update keyed state signals if item references differ
|
|
898
|
+
if (keyedState) {
|
|
899
|
+
if (newItems[i1] !== oldItems[i1]) {
|
|
900
|
+
const k = keyFn(newItems[i1]);
|
|
901
|
+
const entry = keyedState.get(k);
|
|
902
|
+
if (entry) entry.itemSig.set(newItems[i1]);
|
|
903
|
+
}
|
|
904
|
+
if (newItems[i2] !== oldItems[i2]) {
|
|
905
|
+
const k = keyFn(newItems[i2]);
|
|
906
|
+
const entry = keyedState.get(k);
|
|
907
|
+
if (entry) entry.itemSig.set(newItems[i2]);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// DOM moves: swap the two items' DOM ranges.
|
|
912
|
+
// Adjacent swaps need special handling because moving item2 before
|
|
913
|
+
// item1 invalidates the pre-computed end boundary for item1 (it was
|
|
914
|
+
// item2's marker, which has now moved). For adjacent items, a single
|
|
915
|
+
// _moveItem suffices. For non-adjacent items, we recompute end1 after
|
|
916
|
+
// the first move.
|
|
917
|
+
const isAdjacent = (i2 === i1 + 1) || (i1 === i2 + 1);
|
|
918
|
+
const lo = Math.min(i1, i2), hi = Math.max(i1, i2);
|
|
919
|
+
|
|
920
|
+
if (isAdjacent) {
|
|
921
|
+
// Adjacent: just move the later item's nodes before the earlier item's marker.
|
|
922
|
+
const endHi = _findNextMarkerAfter(parent, mappedNodes[hi], mappedNodes, hi, endMarker);
|
|
923
|
+
_moveItem(parent, mappedNodes[hi], endHi, mappedNodes[lo]);
|
|
924
|
+
} else {
|
|
925
|
+
// Non-adjacent: use a placeholder to remember i2's position, then
|
|
926
|
+
// recompute end1 after the first move (since DOM has changed).
|
|
927
|
+
const end2 = _findNextMarkerAfter(parent, mappedNodes[i2], mappedNodes, i2, endMarker);
|
|
928
|
+
|
|
929
|
+
const placeholder = document.createComment('tmp');
|
|
930
|
+
parent.insertBefore(placeholder, mappedNodes[i2]);
|
|
931
|
+
|
|
932
|
+
// Move i2's nodes to before i1's current position
|
|
933
|
+
_moveItem(parent, mappedNodes[i2], end2, mappedNodes[i1]);
|
|
934
|
+
// Recompute end1 — the DOM has changed, so the pre-move boundary is stale
|
|
935
|
+
const end1 = _findNextMarkerAfter(parent, mappedNodes[i1], mappedNodes, i1, endMarker);
|
|
936
|
+
// Move i1's nodes to where i2 was (before placeholder)
|
|
937
|
+
_moveItem(parent, mappedNodes[i1], end1, placeholder);
|
|
938
|
+
parent.removeChild(placeholder);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
_copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// --- Fast path B: Single item relocated ---
|
|
947
|
+
// One item removed from position `from` and inserted at position `to`,
|
|
948
|
+
// everything between shifted by one.
|
|
949
|
+
if (mismatchCount >= 2 && mismatchCount <= midNewLen) {
|
|
950
|
+
// Try to detect single-move pattern:
|
|
951
|
+
// If we remove element at `from` in old and insert at `to` in new,
|
|
952
|
+
// the rest should match.
|
|
953
|
+
// Forward move: old[from] = new[to], old[from+1..to] = new[from..to-1]
|
|
954
|
+
// Backward move: old[from] = new[to], old[to..from-1] = new[to+1..from]
|
|
955
|
+
|
|
956
|
+
const fromRel = mm1; // first mismatch - the moved item was here in old OR went here in new
|
|
957
|
+
let movedKey = null;
|
|
958
|
+
let fromAbs = -1, toAbs = -1;
|
|
959
|
+
let isMove = false;
|
|
960
|
+
|
|
961
|
+
// Check forward move: item at old[start+fromRel] moved later
|
|
962
|
+
const candidateKey = keyFn(oldItems[start + fromRel]);
|
|
963
|
+
// Find where this key ended up in new
|
|
964
|
+
let destRel = -1;
|
|
965
|
+
for (let i = fromRel; i < midNewLen; i++) {
|
|
966
|
+
if (keyFn(newItems[start + i]) === candidateKey) { destRel = i; break; }
|
|
967
|
+
}
|
|
968
|
+
if (destRel > fromRel) {
|
|
969
|
+
// Verify: old[fromRel+1..destRel] should match new[fromRel..destRel-1]
|
|
970
|
+
let match = true;
|
|
971
|
+
for (let i = fromRel; i < destRel; i++) {
|
|
972
|
+
if (keyFn(oldItems[start + i + 1]) !== keyFn(newItems[start + i])) { match = false; break; }
|
|
973
|
+
}
|
|
974
|
+
if (match) {
|
|
975
|
+
// And everything after destRel should be the same
|
|
976
|
+
let afterMatch = true;
|
|
977
|
+
for (let i = destRel + 1; i < midNewLen; i++) {
|
|
978
|
+
if (keyFn(oldItems[start + i]) !== keyFn(newItems[start + i])) { afterMatch = false; break; }
|
|
979
|
+
}
|
|
980
|
+
if (afterMatch) {
|
|
981
|
+
isMove = true;
|
|
982
|
+
fromAbs = start + fromRel;
|
|
983
|
+
toAbs = start + destRel;
|
|
984
|
+
movedKey = candidateKey;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (!isMove) {
|
|
990
|
+
// Check backward move: item from later in old moved to start+fromRel in new
|
|
991
|
+
const candidateKey2 = keyFn(newItems[start + fromRel]);
|
|
992
|
+
let srcRel = -1;
|
|
993
|
+
for (let i = fromRel; i < midOldLen; i++) {
|
|
994
|
+
if (keyFn(oldItems[start + i]) === candidateKey2) { srcRel = i; break; }
|
|
995
|
+
}
|
|
996
|
+
if (srcRel > fromRel) {
|
|
997
|
+
// Verify: old[fromRel..srcRel-1] should match new[fromRel+1..srcRel]
|
|
998
|
+
let match = true;
|
|
999
|
+
for (let i = fromRel; i < srcRel; i++) {
|
|
1000
|
+
if (keyFn(oldItems[start + i]) !== keyFn(newItems[start + i + 1])) { match = false; break; }
|
|
1001
|
+
}
|
|
1002
|
+
if (match) {
|
|
1003
|
+
let afterMatch = true;
|
|
1004
|
+
for (let i = srcRel + 1; i < midNewLen; i++) {
|
|
1005
|
+
if (keyFn(oldItems[start + i]) !== keyFn(newItems[start + i])) { afterMatch = false; break; }
|
|
1006
|
+
}
|
|
1007
|
+
if (afterMatch) {
|
|
1008
|
+
isMove = true;
|
|
1009
|
+
fromAbs = start + srcRel;
|
|
1010
|
+
toAbs = start + fromRel;
|
|
1011
|
+
movedKey = candidateKey2;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (isMove) {
|
|
1018
|
+
// Copy all mapped/dispose to new arrays
|
|
1019
|
+
for (let i = start; i <= oldEnd; i++) {
|
|
1020
|
+
newMapped[i] = mappedNodes[i];
|
|
1021
|
+
newDispose[i] = disposeFns[i];
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Shift entries in newMapped/newDispose to reflect the move
|
|
1025
|
+
const movedMarker = newMapped[fromAbs];
|
|
1026
|
+
const movedDispose = newDispose[fromAbs];
|
|
1027
|
+
|
|
1028
|
+
if (fromAbs < toAbs) {
|
|
1029
|
+
// Forward move: shift [from+1..to] left by 1
|
|
1030
|
+
for (let i = fromAbs; i < toAbs; i++) {
|
|
1031
|
+
newMapped[i] = newMapped[i + 1];
|
|
1032
|
+
newDispose[i] = newDispose[i + 1];
|
|
1033
|
+
}
|
|
1034
|
+
} else {
|
|
1035
|
+
// Backward move: shift [to..from-1] right by 1
|
|
1036
|
+
for (let i = fromAbs; i > toAbs; i--) {
|
|
1037
|
+
newMapped[i] = newMapped[i - 1];
|
|
1038
|
+
newDispose[i] = newDispose[i - 1];
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
newMapped[toAbs] = movedMarker;
|
|
1042
|
+
newDispose[toAbs] = movedDispose;
|
|
1043
|
+
|
|
1044
|
+
// Update keyed state signals for items whose references changed
|
|
1045
|
+
if (keyedState) {
|
|
1046
|
+
for (let i = start; i <= newEnd; i++) {
|
|
1047
|
+
const key = keyFn(newItems[i]);
|
|
1048
|
+
if (newItems[i] !== oldItems[i]) {
|
|
1049
|
+
// Only look up oldItems[i] by key if index is in old range
|
|
1050
|
+
const entry = keyedState.get(key);
|
|
1051
|
+
if (entry) entry.itemSig.set(newItems[i]);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Single DOM move: move the item's nodes to its new position
|
|
1057
|
+
const movedEnd = _findNextMarkerAfter(parent, movedMarker, mappedNodes, fromAbs, endMarker);
|
|
1058
|
+
// Find the reference node: the marker of the item that should come AFTER the moved item
|
|
1059
|
+
let ref;
|
|
1060
|
+
if (toAbs + 1 < newLen) {
|
|
1061
|
+
ref = newMapped[toAbs + 1];
|
|
1062
|
+
} else {
|
|
1063
|
+
ref = endMarker;
|
|
1064
|
+
}
|
|
1065
|
+
// For suffix items, use the actual mapped marker
|
|
1066
|
+
if (toAbs >= newEnd + 1 || (ref && ref.parentNode !== parent)) {
|
|
1067
|
+
ref = endMarker;
|
|
1068
|
+
}
|
|
1069
|
+
_moveItem(parent, movedMarker, movedEnd, ref);
|
|
1070
|
+
|
|
1071
|
+
_copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
799
1077
|
// --- General case: reconcile middle section ---
|
|
800
|
-
// Build old key → old index map for middle section only
|
|
801
1078
|
const oldKeyMap = new Map();
|
|
802
1079
|
for (let i = start; i <= oldEnd; i++) {
|
|
803
1080
|
oldKeyMap.set(keyFn(oldItems[i]), i);
|
|
@@ -806,7 +1083,6 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
806
1083
|
const oldIndices = new Int32Array(midNewLen);
|
|
807
1084
|
oldIndices.fill(-1);
|
|
808
1085
|
|
|
809
|
-
// Match by key
|
|
810
1086
|
for (let i = start; i <= newEnd; i++) {
|
|
811
1087
|
const key = keyFn(newItems[i]);
|
|
812
1088
|
const oldIdx = oldKeyMap.get(key);
|
|
@@ -815,43 +1091,35 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
815
1091
|
newMapped[i] = mappedNodes[oldIdx];
|
|
816
1092
|
newDispose[i] = disposeFns[oldIdx];
|
|
817
1093
|
oldIndices[i - start] = oldIdx;
|
|
818
|
-
// Update item signal if reference changed (non-raw mode only)
|
|
819
1094
|
if (keyedState && newItems[i] !== oldItems[oldIdx]) {
|
|
820
1095
|
keyedState.get(key).itemSig.set(newItems[i]);
|
|
821
1096
|
}
|
|
822
1097
|
}
|
|
823
1098
|
}
|
|
824
1099
|
|
|
825
|
-
// Dispose removed items
|
|
826
|
-
|
|
1100
|
+
// Dispose removed items (iterate in reverse to avoid shifting boundaries)
|
|
1101
|
+
const removedIndices = [...oldKeyMap.values()].sort((a, b) => b - a);
|
|
1102
|
+
for (const oldIdx of removedIndices) {
|
|
827
1103
|
disposeFns[oldIdx]?.();
|
|
828
|
-
|
|
829
|
-
|
|
1104
|
+
// Compute the range boundary from the live DOM. Adjacent removals can
|
|
1105
|
+
// detach mappedNodes[oldIdx + 1] before we get here, so we cannot trust
|
|
1106
|
+
// that reference — walk the DOM to find the next surviving item marker.
|
|
1107
|
+
const rangeEnd = _findNextMarkerAfter(parent, mappedNodes[oldIdx], mappedNodes, oldIdx, endMarker);
|
|
1108
|
+
_removeItemNodes(parent, mappedNodes[oldIdx], rangeEnd);
|
|
1109
|
+
if (keyedState) keyedState.delete(keyFn(oldItems[oldIdx]));
|
|
830
1110
|
}
|
|
831
1111
|
|
|
832
|
-
// Create new items
|
|
1112
|
+
// Create new items (into a detached fragment, then positioned below)
|
|
833
1113
|
for (let i = start; i <= newEnd; i++) {
|
|
834
1114
|
if (!newMapped[i]) {
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
const key = keyFn(item);
|
|
840
|
-
const itemSig = signal(item);
|
|
841
|
-
accessor = itemSig;
|
|
842
|
-
keyedState.set(key, { itemSig });
|
|
843
|
-
} else {
|
|
844
|
-
accessor = item;
|
|
845
|
-
}
|
|
846
|
-
newMapped[i] = _createItemScope(dispose => {
|
|
847
|
-
newDispose[idx] = dispose;
|
|
848
|
-
return mapFn(accessor, idx);
|
|
849
|
-
});
|
|
1115
|
+
const frag = document.createDocumentFragment();
|
|
1116
|
+
_createKeyedItem(frag, newItems[i], i, keyFn, keyedState, mapFn, newMapped, newDispose, signal);
|
|
1117
|
+
// Leave in frag for now — will be positioned in the move pass
|
|
1118
|
+
newMapped[i]._frag = frag;
|
|
850
1119
|
}
|
|
851
1120
|
}
|
|
852
1121
|
|
|
853
1122
|
// Position using LIS
|
|
854
|
-
// First check: are reused items already in order? (common for update-in-place)
|
|
855
1123
|
let reusedCount = 0;
|
|
856
1124
|
let alreadySorted = true;
|
|
857
1125
|
let lastOldIdx = -1;
|
|
@@ -866,7 +1134,6 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
866
1134
|
const inLIS = new Uint8Array(midNewLen);
|
|
867
1135
|
|
|
868
1136
|
if (alreadySorted) {
|
|
869
|
-
// All reused items are in order — mark all as in LIS (no moves needed)
|
|
870
1137
|
for (let i = 0; i < midNewLen; i++) {
|
|
871
1138
|
if (oldIndices[i] !== -1) inLIS[i] = 1;
|
|
872
1139
|
}
|
|
@@ -891,21 +1158,48 @@ function reconcileKeyed(parent, endMarker, oldItems, newItems, mappedNodes, disp
|
|
|
891
1158
|
}
|
|
892
1159
|
}
|
|
893
1160
|
|
|
894
|
-
// Position: work backwards,
|
|
895
|
-
|
|
896
|
-
|
|
1161
|
+
// Position: work backwards, move items not in LIS
|
|
1162
|
+
// For existing items: move all nodes from marker to next-item boundary.
|
|
1163
|
+
// For new items: insert from their detached fragment.
|
|
1164
|
+
// We rebuild the output array to reflect final positions.
|
|
1165
|
+
_copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen);
|
|
897
1166
|
|
|
1167
|
+
// Start ref at the first suffix item's marker (not endMarker) so moved items
|
|
1168
|
+
// land before the suffix, not after it.
|
|
1169
|
+
let ref = newEnd + 1 < newLen && mappedNodes[newEnd + 1]
|
|
1170
|
+
? mappedNodes[newEnd + 1] : endMarker;
|
|
898
1171
|
for (let i = newEnd; i >= start; i--) {
|
|
899
1172
|
const mi = i - start;
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1173
|
+
const marker = mappedNodes[i];
|
|
1174
|
+
|
|
1175
|
+
if (oldIndices[mi] === -1) {
|
|
1176
|
+
// New item — insert from detached fragment
|
|
1177
|
+
if (marker._frag) {
|
|
1178
|
+
parent.insertBefore(marker._frag, ref);
|
|
1179
|
+
delete marker._frag;
|
|
1180
|
+
}
|
|
1181
|
+
} else if (!inLIS[mi]) {
|
|
1182
|
+
// Existing item not in LIS — move all its nodes
|
|
1183
|
+
const nextItemMarker = _findNextMarkerAfter(parent, marker, mappedNodes, i, endMarker);
|
|
1184
|
+
_moveItem(parent, marker, nextItemMarker, ref);
|
|
904
1185
|
}
|
|
905
|
-
|
|
1186
|
+
ref = marker;
|
|
906
1187
|
}
|
|
1188
|
+
}
|
|
907
1189
|
|
|
908
|
-
|
|
1190
|
+
// TODO(perf): cache item end boundary on marker if large keyed reorders show O(n²) hot paths.
|
|
1191
|
+
// Find the boundary end for an item's nodes in the current DOM.
|
|
1192
|
+
// Walks from the marker's nextSibling until we hit another item's marker or endMarker.
|
|
1193
|
+
function _findNextMarkerAfter(parent, marker, mappedNodes, idx, endMarker) {
|
|
1194
|
+
// The item's nodes end at the next sibling that is either:
|
|
1195
|
+
// - another item's marker comment (data === 'i')
|
|
1196
|
+
// - the list endMarker (data === '/list')
|
|
1197
|
+
let n = marker.nextSibling;
|
|
1198
|
+
while (n && n !== endMarker) {
|
|
1199
|
+
if (n.nodeType === 8 && n.data === 'i') return n;
|
|
1200
|
+
n = n.nextSibling;
|
|
1201
|
+
}
|
|
1202
|
+
return endMarker;
|
|
909
1203
|
}
|
|
910
1204
|
|
|
911
1205
|
function _copyBack(mappedNodes, disposeFns, newMapped, newDispose, newLen) {
|
|
@@ -933,18 +1227,31 @@ export function spread(el, props) {
|
|
|
933
1227
|
}
|
|
934
1228
|
|
|
935
1229
|
if (typeof value === 'function' && !key.startsWith('on')) {
|
|
936
|
-
// Reactive prop — create micro-effect
|
|
1230
|
+
// Reactive prop — create micro-effect. The disposer must be registered
|
|
1231
|
+
// on el._propEffects so disposeTree() (dom.js) tears it down when the
|
|
1232
|
+
// element unmounts; otherwise the effect keeps firing on signal writes
|
|
1233
|
+
// for a detached element. Mirror the setProp() pattern.
|
|
1234
|
+
if (!el._propEffects) el._propEffects = {};
|
|
1235
|
+
// If a previous spread/setProp already registered an effect for this
|
|
1236
|
+
// key, dispose it first to avoid double-tracking.
|
|
1237
|
+
if (el._propEffects[key]) {
|
|
1238
|
+
try { el._propEffects[key](); } catch (e) { /* already disposed */ }
|
|
1239
|
+
}
|
|
937
1240
|
if (key === 'class' || key === 'className') {
|
|
938
|
-
|
|
1241
|
+
el._propEffects[key] = effect(() => {
|
|
1242
|
+
const cls = value() || '';
|
|
1243
|
+
if (_hasSVGElement && el instanceof SVGElement) el.setAttribute('class', cls);
|
|
1244
|
+
else el.className = cls;
|
|
1245
|
+
});
|
|
939
1246
|
} else if (key === 'style' && typeof value() === 'object') {
|
|
940
|
-
effect(() => {
|
|
1247
|
+
el._propEffects[key] = effect(() => {
|
|
941
1248
|
const styles = value();
|
|
942
1249
|
for (const prop in styles) {
|
|
943
1250
|
el.style[prop] = styles[prop] ?? '';
|
|
944
1251
|
}
|
|
945
1252
|
});
|
|
946
1253
|
} else {
|
|
947
|
-
effect(() => { setProp(el, key, value()); });
|
|
1254
|
+
el._propEffects[key] = effect(() => { setProp(el, key, value()); });
|
|
948
1255
|
}
|
|
949
1256
|
} else {
|
|
950
1257
|
// Static prop
|
|
@@ -953,6 +1260,15 @@ export function spread(el, props) {
|
|
|
953
1260
|
}
|
|
954
1261
|
}
|
|
955
1262
|
|
|
1263
|
+
// NOTE: this is the fine-grained-compiler path's setProp. A second
|
|
1264
|
+
// implementation lives in dom.js (h()/diff path). See the longer note above
|
|
1265
|
+
// the dom.js version. Key differences vs. dom.js setProp:
|
|
1266
|
+
// - assumes events are handled by the compiler (delegation or direct
|
|
1267
|
+
// addEventListener) — no el._events bookkeeping here.
|
|
1268
|
+
// - sanitizes URL attributes (href/src) against javascript: protocol.
|
|
1269
|
+
// - enforces innerHTML must be { __html: ... } — plain strings are warned.
|
|
1270
|
+
// Both share the el._propEffects[key] disposer convention so disposeTree()
|
|
1271
|
+
// can tear down reactive prop effects on unmount.
|
|
956
1272
|
export function setProp(el, key, value) {
|
|
957
1273
|
// Ref handling — assign element to ref object/callback (defense in depth)
|
|
958
1274
|
if (key === 'ref') {
|
|
@@ -964,6 +1280,19 @@ export function setProp(el, key, value) {
|
|
|
964
1280
|
// Key prop — no-op, WhatFW has no virtual DOM (defense in depth, issue #6)
|
|
965
1281
|
if (key === 'key') return;
|
|
966
1282
|
|
|
1283
|
+
// Reactive accessor: function values on non-event props are treated as
|
|
1284
|
+
// reactive getters. Wrap in an effect so the prop auto-updates. Track the
|
|
1285
|
+
// disposer on el._propEffects so disposeTree() tears it down on unmount —
|
|
1286
|
+
// mirrors the pattern in dom.js setProp / spread().
|
|
1287
|
+
if (typeof value === 'function' && !key.startsWith('on')) {
|
|
1288
|
+
if (!el._propEffects) el._propEffects = {};
|
|
1289
|
+
if (el._propEffects[key]) {
|
|
1290
|
+
try { el._propEffects[key](); } catch (e) { /* already disposed */ }
|
|
1291
|
+
}
|
|
1292
|
+
el._propEffects[key] = effect(() => setProp(el, key, value()));
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
967
1296
|
// Sanitize URL attributes — reject dangerous protocols
|
|
968
1297
|
if (URL_ATTRS.has(key) || URL_ATTRS.has(key.toLowerCase())) {
|
|
969
1298
|
if (!isSafeUrl(value)) {
|
|
@@ -974,21 +1303,33 @@ export function setProp(el, key, value) {
|
|
|
974
1303
|
}
|
|
975
1304
|
}
|
|
976
1305
|
|
|
1306
|
+
const isSvg = _hasSVGElement && el instanceof SVGElement;
|
|
1307
|
+
|
|
977
1308
|
if (key === 'class' || key === 'className') {
|
|
978
|
-
|
|
1309
|
+
if (isSvg) {
|
|
1310
|
+
el.setAttribute('class', value || '');
|
|
1311
|
+
} else {
|
|
1312
|
+
el.className = value || '';
|
|
1313
|
+
}
|
|
979
1314
|
} else if (key === 'dangerouslySetInnerHTML') {
|
|
980
|
-
|
|
1315
|
+
const html = value?.__html ?? '';
|
|
1316
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__ && typeof html === 'string' && /(<script|onerror\s*=|onload\s*=|javascript:)/i.test(html)) {
|
|
1317
|
+
console.warn('[what] dangerouslySetInnerHTML contains potential XSS vectors. Ensure content is sanitized.');
|
|
1318
|
+
}
|
|
1319
|
+
el.innerHTML = html;
|
|
981
1320
|
} else if (key === 'innerHTML') {
|
|
982
1321
|
if (value && typeof value === 'object' && '__html' in value) {
|
|
983
|
-
|
|
1322
|
+
const html = value.__html ?? '';
|
|
1323
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__ && typeof html === 'string' && /(<script|onerror\s*=|onload\s*=|javascript:)/i.test(html)) {
|
|
1324
|
+
console.warn('[what] dangerouslySetInnerHTML contains potential XSS vectors. Ensure content is sanitized.');
|
|
1325
|
+
}
|
|
1326
|
+
el.innerHTML = html;
|
|
984
1327
|
} else {
|
|
985
|
-
// Plain string innerHTML is rejected for security — use { __html: string } form
|
|
986
1328
|
if (typeof console !== 'undefined' && value != null && value !== '') {
|
|
987
1329
|
console.warn(
|
|
988
1330
|
'[what] Plain string innerHTML is not allowed. Use { __html: "..." } or dangerouslySetInnerHTML={{ __html: "..." }} instead.'
|
|
989
1331
|
);
|
|
990
1332
|
}
|
|
991
|
-
// Ignored — do not set innerHTML from plain string
|
|
992
1333
|
}
|
|
993
1334
|
} else if (key === 'style') {
|
|
994
1335
|
if (typeof value === 'string') {
|
|
@@ -1003,6 +1344,10 @@ export function setProp(el, key, value) {
|
|
|
1003
1344
|
} else if (typeof value === 'boolean') {
|
|
1004
1345
|
if (value) el.setAttribute(key, '');
|
|
1005
1346
|
else el.removeAttribute(key);
|
|
1347
|
+
} else if (isSvg) {
|
|
1348
|
+
el.setAttribute(key, value);
|
|
1349
|
+
} else if (key === 'value' && el.tagName === 'SELECT') {
|
|
1350
|
+
_setSelectValue(el, value);
|
|
1006
1351
|
} else if (key in el) {
|
|
1007
1352
|
el[key] = value;
|
|
1008
1353
|
} else {
|