@barefootjs/client 0.34.0 → 0.35.1
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/csr-adapter.js +37 -29
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/runtime/component.d.ts +29 -7
- package/dist/runtime/component.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +101 -13
- package/dist/runtime/map-array-lazy.d.ts +29 -0
- package/dist/runtime/map-array-lazy.d.ts.map +1 -1
- package/dist/runtime/map-array.d.ts +12 -2
- package/dist/runtime/map-array.d.ts.map +1 -1
- package/dist/runtime/portal.d.ts +44 -0
- package/dist/runtime/portal.d.ts.map +1 -1
- package/dist/runtime/standalone.js +98 -10
- package/dist/runtime/track-position.d.ts +37 -0
- package/dist/runtime/track-position.d.ts.map +1 -0
- package/dist/shims.d.ts +1 -0
- package/dist/shims.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +1 -0
- package/src/runtime/component.ts +33 -7
- package/src/runtime/index.ts +4 -0
- package/src/runtime/map-array-lazy.ts +45 -2
- package/src/runtime/map-array.ts +54 -13
- package/src/runtime/portal.ts +154 -0
- package/src/runtime/track-position.ts +47 -0
- package/src/shims.ts +4 -0
|
@@ -17,6 +17,16 @@
|
|
|
17
17
|
* extras...) triple — moves, mounts, and unmounts as a single unit.
|
|
18
18
|
* Single-root loops continue to flow through the legacy path verbatim.
|
|
19
19
|
*/
|
|
20
|
+
/**
|
|
21
|
+
* Shape `renderItem` callbacks are compiled to. `index` mirrors `item`:
|
|
22
|
+
* an accessor, not a plain number, so a row body's reference to the raw
|
|
23
|
+
* `.map()` index parameter — rewritten to a call by
|
|
24
|
+
* `wrapLoopParamAsAccessor` — reads the row's CURRENT position instead of
|
|
25
|
+
* the value captured when the row was first created (#2859). `existing`
|
|
26
|
+
* stays optional/untyped-per-call-site since `mapArray` and
|
|
27
|
+
* `mapArrayAnchored` return different element shapes.
|
|
28
|
+
*/
|
|
29
|
+
export type RenderItem<T, E, R> = (item: () => T, index: () => number, existing?: E) => R;
|
|
20
30
|
/**
|
|
21
31
|
* Find loop boundary comment markers in a container.
|
|
22
32
|
*
|
|
@@ -73,7 +83,7 @@ export declare function longestIncreasingSubsequenceIndices(arr: number[]): numb
|
|
|
73
83
|
* replaced a hardcoded constant instead of just fixing
|
|
74
84
|
* the value).
|
|
75
85
|
*/
|
|
76
|
-
export declare function mapArray<T>(accessor: () => T[], container: HTMLElement | null, getKey: ((item: T, index: number) => string) | null, renderItem:
|
|
86
|
+
export declare function mapArray<T>(accessor: () => T[], container: HTMLElement | null, getKey: ((item: T, index: number) => string) | null, renderItem: RenderItem<T, HTMLElement, HTMLElement>, markerId?: string, bfId?: string, keyAttrName?: string): void;
|
|
77
87
|
/**
|
|
78
88
|
* Per-item scoped list rendering for whole-item conditionals (#1665).
|
|
79
89
|
*
|
|
@@ -82,5 +92,5 @@ export declare function mapArray<T>(accessor: () => T[], container: HTMLElement
|
|
|
82
92
|
* (hydration). Items may render zero elements; the anchor is the stable
|
|
83
93
|
* identity and position.
|
|
84
94
|
*/
|
|
85
|
-
export declare function mapArrayAnchored<T>(accessor: () => T[], container: HTMLElement | null, getKey: ((item: T, index: number) => string) | null, renderItem:
|
|
95
|
+
export declare function mapArrayAnchored<T>(accessor: () => T[], container: HTMLElement | null, getKey: ((item: T, index: number) => string) | null, renderItem: RenderItem<T, Comment, DocumentFragment | Comment>, markerId?: string, bfId?: string): void;
|
|
86
96
|
//# sourceMappingURL=map-array.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-array.d.ts","sourceRoot":"","sources":["../../src/runtime/map-array.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;
|
|
1
|
+
{"version":3,"file":"map-array.d.ts","sourceRoot":"","sources":["../../src/runtime/map-array.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AA6EH;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,MAAM,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,CAAA;AAEzF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,WAAW,EACtB,QAAQ,CAAC,EAAE,MAAM,GAChB;IAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,CA+BhD;AAyHD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mCAAmC,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CA2B3E;AA+GD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,QAAQ,EAAE,MAAM,CAAC,EAAE,EACnB,SAAS,EAAE,WAAW,GAAG,IAAI,EAC7B,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,IAAI,EACnD,UAAU,EAAE,UAAU,CAAC,CAAC,EAAE,WAAW,EAAE,WAAW,CAAC,EACnD,QAAQ,CAAC,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,MAAM,EACb,WAAW,GAAE,MAAe,GAC3B,IAAI,CA4UN;AA0HD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,QAAQ,EAAE,MAAM,CAAC,EAAE,EACnB,SAAS,EAAE,WAAW,GAAG,IAAI,EAC7B,MAAM,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,IAAI,EACnD,UAAU,EAAE,UAAU,CAAC,CAAC,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,EAC9D,QAAQ,CAAC,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,MAAM,GACZ,IAAI,CAoGN"}
|
package/dist/runtime/portal.d.ts
CHANGED
|
@@ -92,5 +92,49 @@ export declare function isSSRPortal(element: HTMLElement): boolean;
|
|
|
92
92
|
*/
|
|
93
93
|
export declare function findSiblingSlot(el: HTMLElement, slotSelector: string): HTMLElement | null;
|
|
94
94
|
export declare function cleanupPortalPlaceholder(portalId: string): void;
|
|
95
|
+
/**
|
|
96
|
+
* Insertion rule (#2717): the portal's content is appended to `container`
|
|
97
|
+
* synchronously, at call time, and — when the component it belongs to is
|
|
98
|
+
* not yet in the document — re-appended once that component connects,
|
|
99
|
+
* which moves it to the container's end.
|
|
100
|
+
*
|
|
101
|
+
* - Owner already connected (hydration: SSR markup is in the document
|
|
102
|
+
* before `init` runs), or nothing to wait for: a single append, at the
|
|
103
|
+
* container's end — unchanged behaviour.
|
|
104
|
+
* - Owner not yet connected (a client-side mount: `materializeComponent`
|
|
105
|
+
* runs `init` — and so the `ref` callbacks that call this — BEFORE the
|
|
106
|
+
* bare-`createComponent` caller connects the root): the element is
|
|
107
|
+
* appended now all the same, and a reorder is queued; once the owner
|
|
108
|
+
* connects, pending portals are re-appended in creation order.
|
|
109
|
+
*
|
|
110
|
+
* Without the reorder the two construction paths disagree on
|
|
111
|
+
* `document.body`'s child order: hydration yields `[root, …portals]`
|
|
112
|
+
* while a CSR mount yields `[…portals, root]`, because the root is
|
|
113
|
+
* appended AFTER its portals were. Child order is user-visible (paint
|
|
114
|
+
* order between equal-`z-index` overlays, focus traversal, `querySelector`
|
|
115
|
+
* results), so both paths converge on the hydration answer.
|
|
116
|
+
*
|
|
117
|
+
* The append itself is never deferred: the element is connected to the
|
|
118
|
+
* document at every point in time, only its position among the
|
|
119
|
+
* container's children settles asynchronously (as a microtask, before the
|
|
120
|
+
* next paint). Portal consumers measure layout synchronously in the same
|
|
121
|
+
* tick as their `ref` callback — `createEffect`'s first run is synchronous,
|
|
122
|
+
* and the floating-position components (popover, dropdown-menu, context-
|
|
123
|
+
* menu, …) read `offsetWidth`/`offsetHeight` of the portaled element there,
|
|
124
|
+
* gated only on their open signal — so a portal that was briefly absent
|
|
125
|
+
* from the document would hand them zero-sized boxes with nothing to
|
|
126
|
+
* re-trigger the measurement once it landed.
|
|
127
|
+
*
|
|
128
|
+
* The subject whose connection is awaited is the `ownerScope` when given
|
|
129
|
+
* and it lies outside the element; otherwise the element's former parent.
|
|
130
|
+
* The self-owner shape (`ownerScope === element`: a child component whose
|
|
131
|
+
* own root carries `bf-s`, e.g. DialogOverlay/DialogContent) needs that
|
|
132
|
+
* fallback, since the immediate append connects the element — and so the
|
|
133
|
+
* owner — right away, while the component under construction is the tree
|
|
134
|
+
* it was taken from. An element in a detached tree with no owner (a
|
|
135
|
+
* fragment-root component, whose scope lives on a comment) is the same
|
|
136
|
+
* case. A bare element with neither has nothing that will ever connect
|
|
137
|
+
* it, so there is nothing to wait for.
|
|
138
|
+
*/
|
|
95
139
|
export declare function createPortal(children: PortalChildren, container?: HTMLElement, options?: PortalOptions): Portal;
|
|
96
140
|
//# sourceMappingURL=portal.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"portal.d.ts","sourceRoot":"","sources":["../../src/runtime/portal.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,MAAM,MAAM,MAAM,GAAG;IACnB,0BAA0B;IAC1B,OAAO,EAAE,WAAW,CAAA;IACpB,8CAA8C;IAC9C,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,mEAAmE;AACnE,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,IAAI,MAAM,CAAA;CAAE,CAAA;AAE/C,4CAA4C;AAC5C,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,MAAM,GAAG,UAAU,CAAA;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAEzD;AAED;;;;;GAKG;AACH;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAiBzF;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAK/D;
|
|
1
|
+
{"version":3,"file":"portal.d.ts","sourceRoot":"","sources":["../../src/runtime/portal.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,MAAM,MAAM,MAAM,GAAG;IACnB,0BAA0B;IAC1B,OAAO,EAAE,WAAW,CAAA;IACpB,8CAA8C;IAC9C,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB;AAED,mEAAmE;AACnE,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,IAAI,MAAM,CAAA;CAAE,CAAA;AAE/C,4CAA4C;AAC5C,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,MAAM,GAAG,UAAU,CAAA;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAEzD;AAED;;;;;GAKG;AACH;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAiBzF;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAK/D;AA+FD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,cAAc,EACxB,SAAS,GAAE,WAA2B,EACtC,OAAO,CAAC,EAAE,aAAa,GACtB,MAAM,CA0DR"}
|
|
@@ -2020,6 +2020,11 @@ function escapeText(value) {
|
|
|
2020
2020
|
return "";
|
|
2021
2021
|
return escapeAttr(value);
|
|
2022
2022
|
}
|
|
2023
|
+
function escapeCommentText(value) {
|
|
2024
|
+
if (value == null)
|
|
2025
|
+
return "";
|
|
2026
|
+
return String(value).replace(/-/g, "‐");
|
|
2027
|
+
}
|
|
2023
2028
|
var BF_MARKUP_BRAND = "__bfMarkup";
|
|
2024
2029
|
function bfMarkup(html) {
|
|
2025
2030
|
return { [BF_MARKUP_BRAND]: html };
|
|
@@ -2153,6 +2158,46 @@ function cleanupPortalPlaceholder(portalId) {
|
|
|
2153
2158
|
const placeholder = document.querySelector(`template[${BF_PORTAL_PLACEHOLDER}="${portalId}"]`);
|
|
2154
2159
|
placeholder?.remove();
|
|
2155
2160
|
}
|
|
2161
|
+
var pendingPortals = [];
|
|
2162
|
+
var pendingObserver = null;
|
|
2163
|
+
function moveToContainerEnd(container, element) {
|
|
2164
|
+
const moveBefore = container.moveBefore;
|
|
2165
|
+
if (typeof moveBefore === "function") {
|
|
2166
|
+
moveBefore.call(container, element, null);
|
|
2167
|
+
} else {
|
|
2168
|
+
container.appendChild(element);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
function flushPendingPortals() {
|
|
2172
|
+
for (const pending of pendingPortals.slice()) {
|
|
2173
|
+
if (!pending.subject.isConnected)
|
|
2174
|
+
continue;
|
|
2175
|
+
pendingPortals.splice(pendingPortals.indexOf(pending), 1);
|
|
2176
|
+
if (pending.element.parentNode === pending.container) {
|
|
2177
|
+
moveToContainerEnd(pending.container, pending.element);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if (pendingPortals.length === 0 && pendingObserver) {
|
|
2181
|
+
pendingObserver.disconnect();
|
|
2182
|
+
pendingObserver = null;
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
function enqueuePendingPortal(pending) {
|
|
2186
|
+
pendingPortals.push(pending);
|
|
2187
|
+
if (!pendingObserver) {
|
|
2188
|
+
pendingObserver = new MutationObserver(flushPendingPortals);
|
|
2189
|
+
pendingObserver.observe(document, { childList: true, subtree: true });
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
function cancelPendingPortal(element) {
|
|
2193
|
+
const idx = pendingPortals.findIndex((p) => p.element === element);
|
|
2194
|
+
if (idx >= 0)
|
|
2195
|
+
pendingPortals.splice(idx, 1);
|
|
2196
|
+
if (pendingPortals.length === 0 && pendingObserver) {
|
|
2197
|
+
pendingObserver.disconnect();
|
|
2198
|
+
pendingObserver = null;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2156
2201
|
function createPortal(children, container = document.body, options) {
|
|
2157
2202
|
let element;
|
|
2158
2203
|
if (children instanceof HTMLElement) {
|
|
@@ -2174,16 +2219,35 @@ function createPortal(children, container = document.body, options) {
|
|
|
2174
2219
|
element.setAttribute(BF_PORTAL_OWNER, scopeId);
|
|
2175
2220
|
}
|
|
2176
2221
|
}
|
|
2222
|
+
const owner = options?.ownerScope;
|
|
2223
|
+
const formerParent = children instanceof HTMLElement ? children.parentNode : null;
|
|
2224
|
+
const subject = owner && !element.contains(owner) ? owner : formerParent instanceof Element ? formerParent : null;
|
|
2177
2225
|
container.appendChild(element);
|
|
2226
|
+
if (subject && !subject.isConnected && typeof MutationObserver !== "undefined") {
|
|
2227
|
+
enqueuePendingPortal({ element, container, subject });
|
|
2228
|
+
}
|
|
2178
2229
|
return {
|
|
2179
2230
|
element,
|
|
2180
2231
|
unmount() {
|
|
2232
|
+
cancelPendingPortal(element);
|
|
2181
2233
|
if (element.parentNode) {
|
|
2182
2234
|
element.parentNode.removeChild(element);
|
|
2183
2235
|
}
|
|
2184
2236
|
}
|
|
2185
2237
|
};
|
|
2186
2238
|
}
|
|
2239
|
+
// src/runtime/track-position.ts
|
|
2240
|
+
function trackPosition(update) {
|
|
2241
|
+
update();
|
|
2242
|
+
const onChange = () => update();
|
|
2243
|
+
window.addEventListener("scroll", onChange, true);
|
|
2244
|
+
window.addEventListener("resize", onChange);
|
|
2245
|
+
return () => {
|
|
2246
|
+
window.removeEventListener("scroll", onChange, true);
|
|
2247
|
+
window.removeEventListener("resize", onChange);
|
|
2248
|
+
update();
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2187
2251
|
// src/runtime/loop-markers.ts
|
|
2188
2252
|
function findLoopMarkers(container, markerId) {
|
|
2189
2253
|
let startMarker = null;
|
|
@@ -2454,17 +2518,20 @@ function createItemScope(item, index, renderItem, existingPrimary, existingExtra
|
|
|
2454
2518
|
let primaryEl;
|
|
2455
2519
|
let dispose;
|
|
2456
2520
|
let setItem;
|
|
2521
|
+
let setIndex;
|
|
2457
2522
|
let extras = [];
|
|
2458
2523
|
let startMarker = null;
|
|
2459
2524
|
let scopeComments = null;
|
|
2460
2525
|
createRoot((d) => {
|
|
2461
2526
|
dispose = d;
|
|
2462
2527
|
const [itemAccessor, itemSetter] = createSignal(item);
|
|
2528
|
+
const [indexAccessor, indexSetter] = createSignal(index);
|
|
2463
2529
|
setItem = itemSetter;
|
|
2530
|
+
setIndex = indexSetter;
|
|
2464
2531
|
const ownsRowMount = !existingPrimary && !!rowMount;
|
|
2465
2532
|
const prevRowMount = ownsRowMount ? setRowMountPoint(rowMount) : null;
|
|
2466
2533
|
try {
|
|
2467
|
-
primaryEl = renderItem(itemAccessor,
|
|
2534
|
+
primaryEl = renderItem(itemAccessor, indexAccessor, existingPrimary);
|
|
2468
2535
|
} catch (err) {
|
|
2469
2536
|
const parked = rowMount?.mounted;
|
|
2470
2537
|
if (parked?.parentNode)
|
|
@@ -2497,7 +2564,7 @@ function createItemScope(item, index, renderItem, existingPrimary, existingExtra
|
|
|
2497
2564
|
if (rowMount && !existingPrimary && extras.length > 0 && primaryEl.parentNode) {
|
|
2498
2565
|
primaryEl.remove();
|
|
2499
2566
|
}
|
|
2500
|
-
return { startMarker, primaryEl, extras, scopeComments, dispose, setItem };
|
|
2567
|
+
return { startMarker, primaryEl, extras, scopeComments, dispose, setItem, setIndex };
|
|
2501
2568
|
}
|
|
2502
2569
|
function mapArray(accessor, container, getKey, renderItem, markerId, bfId, keyAttrName2 = BF_KEY) {
|
|
2503
2570
|
if (!container)
|
|
@@ -2575,7 +2642,8 @@ function mapArray(accessor, container, getKey, renderItem, markerId, bfId, keyAt
|
|
|
2575
2642
|
extras: range.extras,
|
|
2576
2643
|
scopeComments: range.scopeComments,
|
|
2577
2644
|
dispose: () => {},
|
|
2578
|
-
setItem: () => {}
|
|
2645
|
+
setItem: () => {},
|
|
2646
|
+
setIndex: () => {}
|
|
2579
2647
|
});
|
|
2580
2648
|
}
|
|
2581
2649
|
}
|
|
@@ -2621,7 +2689,10 @@ function mapArray(accessor, container, getKey, renderItem, markerId, bfId, keyAt
|
|
|
2621
2689
|
newKeys.add(key);
|
|
2622
2690
|
const existing = scopes.get(key);
|
|
2623
2691
|
if (existing) {
|
|
2624
|
-
|
|
2692
|
+
batch(() => {
|
|
2693
|
+
existing.setItem(item);
|
|
2694
|
+
existing.setIndex(i2);
|
|
2695
|
+
});
|
|
2625
2696
|
desiredOrder.push(existing);
|
|
2626
2697
|
} else {
|
|
2627
2698
|
const scope = createItemScope(item, i2, renderItem, undefined, undefined, undefined, undefined, { container, anchor });
|
|
@@ -2719,23 +2790,26 @@ function removeAnchorScope(scope, end) {
|
|
|
2719
2790
|
function createAnchorScope(item, index, key, renderItem, existingAnchor) {
|
|
2720
2791
|
let dispose;
|
|
2721
2792
|
let setItem;
|
|
2793
|
+
let setIndex;
|
|
2722
2794
|
let returned;
|
|
2723
2795
|
createRoot((d) => {
|
|
2724
2796
|
dispose = d;
|
|
2725
2797
|
const [itemAccessor, itemSetter] = createSignal(item);
|
|
2798
|
+
const [indexAccessor, indexSetter] = createSignal(index);
|
|
2726
2799
|
setItem = itemSetter;
|
|
2727
|
-
|
|
2800
|
+
setIndex = indexSetter;
|
|
2801
|
+
returned = renderItem(itemAccessor, indexAccessor, existingAnchor);
|
|
2728
2802
|
return;
|
|
2729
2803
|
});
|
|
2730
2804
|
if (existingAnchor) {
|
|
2731
|
-
return { anchor: existingAnchor, pending: null, dispose, setItem };
|
|
2805
|
+
return { anchor: existingAnchor, pending: null, dispose, setItem, setIndex };
|
|
2732
2806
|
}
|
|
2733
2807
|
const frag = returned;
|
|
2734
2808
|
const anchor = frag.firstChild;
|
|
2735
2809
|
if (anchor && !anchor.nodeValue?.startsWith(ITEM_PREFIX)) {
|
|
2736
2810
|
anchor.nodeValue = loopItemMarker(key);
|
|
2737
2811
|
}
|
|
2738
|
-
return { anchor, pending: frag, dispose, setItem };
|
|
2812
|
+
return { anchor, pending: frag, dispose, setItem, setIndex };
|
|
2739
2813
|
}
|
|
2740
2814
|
function mapArrayAnchored(accessor, container, getKey, renderItem, markerId, bfId) {
|
|
2741
2815
|
if (!container)
|
|
@@ -2786,7 +2860,10 @@ function mapArrayAnchored(accessor, container, getKey, renderItem, markerId, bfI
|
|
|
2786
2860
|
newKeys.add(key);
|
|
2787
2861
|
const existing = scopes.get(key);
|
|
2788
2862
|
if (existing) {
|
|
2789
|
-
|
|
2863
|
+
batch(() => {
|
|
2864
|
+
existing.setItem(item);
|
|
2865
|
+
existing.setIndex(i);
|
|
2866
|
+
});
|
|
2790
2867
|
desiredOrder.push(existing);
|
|
2791
2868
|
} else {
|
|
2792
2869
|
const scope = createAnchorScope(item, i, key, renderItem);
|
|
@@ -2856,6 +2933,7 @@ function mapArrayLazy(accessor, container, getKey, plan, markerId, bfId, keyAttr
|
|
|
2856
2933
|
key,
|
|
2857
2934
|
primaryEl: undefined,
|
|
2858
2935
|
item,
|
|
2936
|
+
index,
|
|
2859
2937
|
refs: null,
|
|
2860
2938
|
last: null
|
|
2861
2939
|
};
|
|
@@ -2885,7 +2963,7 @@ function mapArrayLazy(accessor, container, getKey, plan, markerId, bfId, keyAttr
|
|
|
2885
2963
|
const el = doms[i2];
|
|
2886
2964
|
const ssrKey = el.getAttribute(keyAttrName2);
|
|
2887
2965
|
const key = ssrKey !== null ? ssrKey : getKey ? getKey(items[i2], i2) : String(i2);
|
|
2888
|
-
const entry = { key, primaryEl: el, item: items[i2], refs: null, last: null };
|
|
2966
|
+
const entry = { key, primaryEl: el, item: items[i2], index: i2, refs: null, last: null };
|
|
2889
2967
|
entries.set(key, entry);
|
|
2890
2968
|
list.push(entry);
|
|
2891
2969
|
}
|
|
@@ -2940,11 +3018,19 @@ function mapArrayLazy(accessor, container, getKey, plan, markerId, bfId, keyAttr
|
|
|
2940
3018
|
newKeys.add(key);
|
|
2941
3019
|
const existing = entries.get(key);
|
|
2942
3020
|
if (existing) {
|
|
2943
|
-
|
|
3021
|
+
const itemChanged = !Object.is(existing.item, item);
|
|
3022
|
+
const indexChanged = plan.indexDriven === true && existing.index !== i2;
|
|
3023
|
+
if (itemChanged) {
|
|
2944
3024
|
const prevItem = existing.item;
|
|
2945
3025
|
existing.item = item;
|
|
3026
|
+
existing.index = i2;
|
|
2946
3027
|
untrack(() => plan.applyItem(existing, prevItem));
|
|
2947
3028
|
markStranded();
|
|
3029
|
+
} else if (indexChanged) {
|
|
3030
|
+
existing.index = i2;
|
|
3031
|
+
untrack(() => plan.applyItem(existing, item));
|
|
3032
|
+
} else {
|
|
3033
|
+
existing.index = i2;
|
|
2948
3034
|
}
|
|
2949
3035
|
desiredOrder.push(existing);
|
|
2950
3036
|
} else {
|
|
@@ -3801,6 +3887,7 @@ export {
|
|
|
3801
3887
|
upsertChild,
|
|
3802
3888
|
unwrap,
|
|
3803
3889
|
untrack,
|
|
3890
|
+
trackPosition,
|
|
3804
3891
|
textOrNode,
|
|
3805
3892
|
textNodeAfterComment as tAfter,
|
|
3806
3893
|
styleToCss,
|
|
@@ -3854,6 +3941,7 @@ export {
|
|
|
3854
3941
|
escapeTextOrNode,
|
|
3855
3942
|
escapeTextOrMarkup,
|
|
3856
3943
|
escapeText,
|
|
3944
|
+
escapeCommentText,
|
|
3857
3945
|
escapeAttr,
|
|
3858
3946
|
endTurn,
|
|
3859
3947
|
disposeScope,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BarefootJS - Floating-element position tracking
|
|
3
|
+
*
|
|
4
|
+
* Keeps a `position: fixed` overlay (menu, popover, listbox, hover card)
|
|
5
|
+
* anchored to its trigger for as long as it is open. Shared by every
|
|
6
|
+
* site/ui overlay that positions itself from `getBoundingClientRect()`
|
|
7
|
+
* so the decision below is made in one place (#2848).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Run `update` now, re-run it on every scroll (capture phase, so a
|
|
11
|
+
* nested scroll container counts too) and on resize, and return the
|
|
12
|
+
* dispose that detaches both listeners.
|
|
13
|
+
*
|
|
14
|
+
* The dispose re-runs `update` ONCE, synchronously, before detaching —
|
|
15
|
+
* that final sample is the whole point of this helper. `scroll` events
|
|
16
|
+
* are coalesced per rendering frame and report the scroll position at
|
|
17
|
+
* dispatch time, not at scroll time. A programmatic scroll that landed
|
|
18
|
+
* in the current frame (a `focus()` on an offscreen item, a
|
|
19
|
+
* `scrollIntoView()`) has therefore not dispatched yet when a close
|
|
20
|
+
* runs in the same frame; the listener is gone by the time the event
|
|
21
|
+
* fires, and whatever position the listener would have written is lost.
|
|
22
|
+
* Without the final sample the closed element's inline position depends
|
|
23
|
+
* on whether a frame boundary happened to fall between that scroll and
|
|
24
|
+
* the close — measured as the `dropdown-menu` idempotence oracle
|
|
25
|
+
* landing on `top: -580px` / `-606px` / `33px` for the same action
|
|
26
|
+
* sequence. Sampling once at dispose makes the closed position a
|
|
27
|
+
* function of the geometry at close time only.
|
|
28
|
+
*
|
|
29
|
+
* (An `overflow: hidden` scroll lock does not narrow this window: it
|
|
30
|
+
* blocks user gestures, never programmatic scrolling, on `html` and
|
|
31
|
+
* `body` alike — verified in Chromium against the fixture-hydrate host.)
|
|
32
|
+
*
|
|
33
|
+
* @param update - Positions the element from current geometry.
|
|
34
|
+
* @returns Dispose: re-runs `update` once, then detaches the listeners.
|
|
35
|
+
*/
|
|
36
|
+
export declare function trackPosition(update: () => void): () => void;
|
|
37
|
+
//# sourceMappingURL=track-position.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"track-position.d.ts","sourceRoot":"","sources":["../../src/runtime/track-position.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAU5D"}
|
package/dist/shims.d.ts
CHANGED
|
@@ -18,4 +18,5 @@ export declare function createPortal(_children: PortalChildren, _container?: Ele
|
|
|
18
18
|
export declare function isSSRPortal(_element: HTMLElement): boolean;
|
|
19
19
|
export declare function findSiblingSlot(_el: HTMLElement, _slotSelector: string): HTMLElement | null;
|
|
20
20
|
export declare function cleanupPortalPlaceholder(_portalId: string): void;
|
|
21
|
+
export declare function trackPosition(_update: () => void): () => void;
|
|
21
22
|
//# sourceMappingURL=shims.d.ts.map
|
package/dist/shims.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shims.d.ts","sourceRoot":"","sources":["../src/shims.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEhF,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAS5F,wBAAgB,UAAU,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAErD;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAEvE;AAED,wBAAgB,YAAY,CAC1B,SAAS,EAAE,cAAc,EACzB,UAAU,CAAC,EAAE,OAAO,EACpB,QAAQ,CAAC,EAAE,aAAa,GACvB,MAAM,CAER;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAE1D;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,WAAW,EAChB,aAAa,EAAE,MAAM,GACpB,WAAW,GAAG,IAAI,CAEpB;AAED,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEhE"}
|
|
1
|
+
{"version":3,"file":"shims.d.ts","sourceRoot":"","sources":["../src/shims.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEhF,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AAS5F,wBAAgB,UAAU,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAErD;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAEvE;AAED,wBAAgB,YAAY,CAC1B,SAAS,EAAE,cAAc,EACzB,UAAU,CAAC,EAAE,OAAO,EACpB,QAAQ,CAAC,EAAE,aAAa,GACvB,MAAM,CAER;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,GAAG,OAAO,CAE1D;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,WAAW,EAChB,aAAa,EAAE,MAAM,GACpB,WAAW,GAAG,IAAI,CAEpB;AAED,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEhE;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAE7D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.1",
|
|
4
4
|
"description": "BarefootJS client package: reactive primitives (SSR-safe) plus browser runtime under the `/runtime` subpath (compiler target)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"directory": "packages/client"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@barefootjs/shared": "0.
|
|
58
|
+
"@barefootjs/shared": "0.35.1"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@barefootjs/jsx": ">=0.2.0"
|
package/src/index.ts
CHANGED
package/src/runtime/component.ts
CHANGED
|
@@ -951,6 +951,33 @@ export function escapeText(value: unknown): string {
|
|
|
951
951
|
return escapeAttr(value)
|
|
952
952
|
}
|
|
953
953
|
|
|
954
|
+
/**
|
|
955
|
+
* Neutralize a value for splicing into an HTML COMMENT (`<!--...-->`), the
|
|
956
|
+
* whole-item loop conditional's `bf-loop-i:<key>` anchor (#1665,
|
|
957
|
+
* `itemAnchorTemplate` in `ir-to-client-js/html-template.ts`) being the one
|
|
958
|
+
* caller today. Standard HTML escaping (`escapeAttr`/`escapeText`) does
|
|
959
|
+
* NOT help here: `-`, `<`, `>` are not special in comment content the way
|
|
960
|
+
* `& < > " '` are in text/attributes — the only thing that terminates a
|
|
961
|
+
* comment early is the literal three-character sequence `-->`, which
|
|
962
|
+
* `escapeAttr` never touches.
|
|
963
|
+
*
|
|
964
|
+
* The key's exact text does not need to round-trip: nothing reads it back
|
|
965
|
+
* out of the DOM (`mapArrayAnchored` matches items positionally on first
|
|
966
|
+
* hydration and by its own JS-computed key afterward, never by re-parsing
|
|
967
|
+
* `Comment.nodeValue`, `map-array.ts`'s `isItemAnchor`/`findItemAnchors`).
|
|
968
|
+
* So replacing every ASCII hyphen with the visually-similar U+2010 HYPHEN
|
|
969
|
+
* is sufficient and simpler than a reversible escape: it makes `-->`
|
|
970
|
+
* unconstructible from the result (a hyphen adjacent to the template's own
|
|
971
|
+
* literal `-->` can no longer complete the sequence), and — because it is
|
|
972
|
+
* a different Unicode character rather than an HTML entity — needs no
|
|
973
|
+
* decoding: HTML comments do not interpret character references at all,
|
|
974
|
+
* so `-` would appear as literal text `-`, not `-`.
|
|
975
|
+
*/
|
|
976
|
+
export function escapeCommentText(value: unknown): string {
|
|
977
|
+
if (value == null) return ''
|
|
978
|
+
return String(value).replace(/-/g, '‐')
|
|
979
|
+
}
|
|
980
|
+
|
|
954
981
|
/**
|
|
955
982
|
* Brand carried by `bfMarkup()` — see that function's docstring. A string
|
|
956
983
|
* key (not a `Symbol`) because the brand must survive `structuredClone`
|
|
@@ -977,7 +1004,7 @@ export interface BfMarkup {
|
|
|
977
1004
|
* itself assembled from a JSX element passed at a non-`children` component
|
|
978
1005
|
* prop position (`header={<strong>Title</strong>}`) — every text segment
|
|
979
1006
|
* inside that assembly was already escaped node-by-node during the
|
|
980
|
-
* assembly (the same `escapeHtml` / `
|
|
1007
|
+
* assembly (the same `escapeHtml` calls / `spliceChildValue` door
|
|
981
1008
|
* `html-template.ts` uses for ordinary element children), so the
|
|
982
1009
|
* concatenated result is exactly as safe as any other compiler-emitted
|
|
983
1010
|
* template fragment.
|
|
@@ -1009,8 +1036,8 @@ export function isBfMarkup(value: unknown): value is BfMarkup {
|
|
|
1009
1036
|
/**
|
|
1010
1037
|
* `escapeText`'s counterpart for a claim-plan slot the compiler has
|
|
1011
1038
|
* classified `kind: 'markup'` at STATIC/initial-render time
|
|
1012
|
-
* (`html-template.ts`'s `
|
|
1013
|
-
* `ctx.dynamicElements` membership `emit-reactive.ts` reads to pick the
|
|
1039
|
+
* (`html-template.ts`'s `spliceChildValue` door, `safe-html.ts`, gated on
|
|
1040
|
+
* the same `ctx.dynamicElements` membership `emit-reactive.ts` reads to pick the
|
|
1014
1041
|
* writer kind for the REACTIVE side, below) — #2651. A `bfMarkup()`-branded
|
|
1015
1042
|
* value is compiler-built HTML the compiler already escaped piecewise
|
|
1016
1043
|
* while assembling it, and must reach the template raw, unescaped a second
|
|
@@ -1027,9 +1054,8 @@ export function escapeTextOrMarkup(value: unknown): string {
|
|
|
1027
1054
|
|
|
1028
1055
|
/**
|
|
1029
1056
|
* Nullish guard for a bare `${children}` passthrough splice
|
|
1030
|
-
* (`ir-to-client-js/html
|
|
1031
|
-
* —
|
|
1032
|
-
* describes) — #2775. The value here is ALREADY-STRINGIFIED MARKUP, not an
|
|
1057
|
+
* (`ir-to-client-js/safe-html.ts`'s `spliceChildValue` door, no-`slotId`
|
|
1058
|
+
* `'expression'` branches) — #2775. The value here is ALREADY-STRINGIFIED MARKUP, not an
|
|
1033
1059
|
* arbitrary prop: `materializeComponent` (this file, "Template functions
|
|
1034
1060
|
* expect children as an HTML string, not an array") joins a component's
|
|
1035
1061
|
* `children` into an HTML string before the template lambda ever runs, so by
|
|
@@ -1063,7 +1089,7 @@ export function markupOrEmpty(value: unknown): string {
|
|
|
1063
1089
|
* interpret HTML, so a raw un-escaped string is an injection/corruption
|
|
1064
1090
|
* risk exactly where the initial SSR/CSR TEMPLATE already calls
|
|
1065
1091
|
* `escapeText` on the same expression (`html-template.ts`'s
|
|
1066
|
-
* `
|
|
1092
|
+
* `spliceChildValue` door, `safe-html.ts`). A live `Node`, by contrast, must pass through
|
|
1067
1093
|
* untouched — `escapeText(node)` would stringify it to garbage, and
|
|
1068
1094
|
* `writeMarkup`'s own `instanceof Node` check needs the real object to
|
|
1069
1095
|
* splice in by identity. This is the single call every "dynamic JSX/text
|
package/src/runtime/index.ts
CHANGED
|
@@ -77,6 +77,9 @@ export {
|
|
|
77
77
|
type PortalChildren,
|
|
78
78
|
} from './portal.ts'
|
|
79
79
|
|
|
80
|
+
// Floating-element positioning (#2848)
|
|
81
|
+
export { trackPosition } from './track-position.ts'
|
|
82
|
+
|
|
80
83
|
// Loop boundary marker lookup (used by mapArray/mapArrayAnchored consumers
|
|
81
84
|
// and compiler-generated clearing code — see ./loop-markers.ts docstring)
|
|
82
85
|
export { getLoopChildren, getLoopNodes } from './loop-markers.ts'
|
|
@@ -105,6 +108,7 @@ export {
|
|
|
105
108
|
parseHTML,
|
|
106
109
|
escapeAttr,
|
|
107
110
|
escapeText,
|
|
111
|
+
escapeCommentText,
|
|
108
112
|
escapeTextOrNode,
|
|
109
113
|
// JSX-element-as-prop markup brand (#2651) — compiler-emitted code only.
|
|
110
114
|
// NOT re-exported from the public `@barefootjs/client` top-level entry;
|
|
@@ -65,6 +65,18 @@
|
|
|
65
65
|
* (a misclassification must be harmless, not silently wrong) and for the
|
|
66
66
|
* three stranding sequences it prevents, each reproduced before it existed.
|
|
67
67
|
*
|
|
68
|
+
* **Index-driven bindings (#2859 follow-up).** A row's position is tracked
|
|
69
|
+
* on `entry.index`, updated on every reconcile whether or not any binding
|
|
70
|
+
* reads it. A plan that sets `indexDriven: true` (emitted when
|
|
71
|
+
* `LazyRowPlanData.readsIndex`) is telling the runtime that some binding of
|
|
72
|
+
* `applyItem` reads `entry.index` — so the reconciler also calls
|
|
73
|
+
* `applyItem` on a pure reorder (item unchanged, position changed), not only
|
|
74
|
+
* on an item change. This mirrors the eager `mapArray` runtime's per-row
|
|
75
|
+
* index SIGNAL (`ItemScope.setIndex`) with no per-row reactive resource: the
|
|
76
|
+
* reconciler already knows a row's position changed the same way it already
|
|
77
|
+
* knows its item changed, so re-delivering that fact through a signal would
|
|
78
|
+
* be exactly the redundant machinery this design exists to avoid.
|
|
79
|
+
*
|
|
68
80
|
* **Plan (compiler-emitted) obligations:**
|
|
69
81
|
* - `createRow` MUST write ALL bindings — item-driven AND outer-involving —
|
|
70
82
|
* with current values (it is CSR creation; it computes everything anyway)
|
|
@@ -126,6 +138,13 @@ export interface LazyRowEntry<T> {
|
|
|
126
138
|
key: string
|
|
127
139
|
primaryEl: HTMLElement
|
|
128
140
|
item: T
|
|
141
|
+
/**
|
|
142
|
+
* The row's current position in the reconciled list (#2859 follow-up).
|
|
143
|
+
* Kept current on EVERY reconcile, whether or not the plan reads it —
|
|
144
|
+
* cheap bookkeeping, and it is what makes a bare reorder (no item change)
|
|
145
|
+
* detectable for `plan.indexDriven` loops.
|
|
146
|
+
*/
|
|
147
|
+
index: number
|
|
129
148
|
/** plan-owned: claimed DOM refs, null until the row's first item-driven write */
|
|
130
149
|
refs: unknown | null
|
|
131
150
|
/** plan-owned: per-binding last-value dedup state */
|
|
@@ -155,6 +174,16 @@ export interface LazyRowPlan<T> {
|
|
|
155
174
|
* nodeValue) to initialize its dedup value and write only where the
|
|
156
175
|
* computed value differs (read-compare-write, spec §9.3(1)). */
|
|
157
176
|
applyOuter?(entries: ReadonlyArray<LazyRowEntry<T>>, seed: boolean): void
|
|
177
|
+
/**
|
|
178
|
+
* True when some binding, condition, or preamble-substituted dependency of
|
|
179
|
+
* this row reads the loop's INDEX (#2859 follow-up,
|
|
180
|
+
* `LazyRowPlanData.readsIndex`). Such a binding is always `readsItem`
|
|
181
|
+
* (compiler-side), so the runtime must also call `applyItem` when a row's
|
|
182
|
+
* POSITION changes with no item change — a plain reorder — not only when
|
|
183
|
+
* `!Object.is(oldItem, newItem)`. Absent/false for every other loop, which
|
|
184
|
+
* keeps a bare reorder exactly as cheap as before this widening.
|
|
185
|
+
*/
|
|
186
|
+
indexDriven?: boolean
|
|
158
187
|
}
|
|
159
188
|
|
|
160
189
|
/**
|
|
@@ -268,6 +297,7 @@ export function mapArrayLazy<T>(
|
|
|
268
297
|
// element and must not read primaryEl.
|
|
269
298
|
primaryEl: undefined as unknown as HTMLElement,
|
|
270
299
|
item,
|
|
300
|
+
index,
|
|
271
301
|
refs: null,
|
|
272
302
|
last: null,
|
|
273
303
|
}
|
|
@@ -307,7 +337,7 @@ export function mapArrayLazy<T>(
|
|
|
307
337
|
// rows carry a depth-suffixed name (#2753 Shape B).
|
|
308
338
|
const ssrKey = el.getAttribute(keyAttrName)
|
|
309
339
|
const key = ssrKey !== null ? ssrKey : getKey ? getKey(items[i], i) : String(i)
|
|
310
|
-
const entry: LazyRowEntry<T> = { key, primaryEl: el, item: items[i], refs: null, last: null }
|
|
340
|
+
const entry: LazyRowEntry<T> = { key, primaryEl: el, item: items[i], index: i, refs: null, last: null }
|
|
311
341
|
entries.set(key, entry)
|
|
312
342
|
list.push(entry)
|
|
313
343
|
}
|
|
@@ -383,11 +413,24 @@ export function mapArrayLazy<T>(
|
|
|
383
413
|
// setItem. `applyItem` runs after `entry.item` is updated, receives
|
|
384
414
|
// the previous item, and is untracked (mixed bindings may read
|
|
385
415
|
// outer signals; the applyOuter effect owns the reactive side).
|
|
386
|
-
|
|
416
|
+
const itemChanged = !Object.is(existing.item, item)
|
|
417
|
+
// A pure reorder (#2859 follow-up): the item is unchanged but the
|
|
418
|
+
// row's POSITION is, and `plan.indexDriven` says some binding reads
|
|
419
|
+
// it. `entry.index` is bookkept below regardless — only the
|
|
420
|
+
// `applyItem` call (and the stranding it implies) is conditional.
|
|
421
|
+
const indexChanged = plan.indexDriven === true && existing.index !== i
|
|
422
|
+
if (itemChanged) {
|
|
387
423
|
const prevItem = existing.item
|
|
388
424
|
existing.item = item
|
|
425
|
+
existing.index = i
|
|
389
426
|
untrack(() => plan.applyItem(existing, prevItem))
|
|
390
427
|
markStranded()
|
|
428
|
+
} else if (indexChanged) {
|
|
429
|
+
existing.index = i
|
|
430
|
+
// prevItem === item here: nothing but the position moved.
|
|
431
|
+
untrack(() => plan.applyItem(existing, item))
|
|
432
|
+
} else {
|
|
433
|
+
existing.index = i
|
|
391
434
|
}
|
|
392
435
|
desiredOrder.push(existing)
|
|
393
436
|
} else {
|