@barefootjs/client 0.21.2 → 0.21.4
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/runtime/index.js +43 -8
- package/dist/runtime/query.d.ts +48 -0
- package/dist/runtime/query.d.ts.map +1 -1
- package/dist/runtime/standalone.js +43 -8
- package/package.json +2 -2
- package/src/runtime/insert.ts +19 -8
- package/src/runtime/query.ts +109 -2
package/dist/runtime/index.js
CHANGED
|
@@ -656,6 +656,21 @@ function isInsideNestedCommentScope(element, scope) {
|
|
|
656
656
|
}
|
|
657
657
|
return false;
|
|
658
658
|
}
|
|
659
|
+
function isInsideNestedChildScope(element, root) {
|
|
660
|
+
let current = element;
|
|
661
|
+
while (current !== root) {
|
|
662
|
+
const parent = current.parentNode;
|
|
663
|
+
if (!parent)
|
|
664
|
+
return false;
|
|
665
|
+
if (parent !== root && parent.nodeType === Node.ELEMENT_NODE && parent.hasAttribute(BF_SCOPE)) {
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
if (isTopLevelCommentScopeNode(current))
|
|
669
|
+
return true;
|
|
670
|
+
current = parent;
|
|
671
|
+
}
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
659
674
|
function isTopLevelCommentScopeNode(node) {
|
|
660
675
|
const parent = node.parentNode;
|
|
661
676
|
if (!parent)
|
|
@@ -712,6 +727,20 @@ function find(scope, selector, ignoreScope) {
|
|
|
712
727
|
return findInPortals(scopeId, selector);
|
|
713
728
|
return null;
|
|
714
729
|
}
|
|
730
|
+
function findCondTarget(scope, selector) {
|
|
731
|
+
const commentInfo = commentScopeRegistry.get(scope);
|
|
732
|
+
if (!commentInfo) {
|
|
733
|
+
return scope.querySelector(selector);
|
|
734
|
+
}
|
|
735
|
+
for (const candidate of candidatesInScope(scope, selector)) {
|
|
736
|
+
if (candidate.parentElement === commentInfo.commentNode.parentElement)
|
|
737
|
+
return candidate;
|
|
738
|
+
const nearestScope = candidate.closest(`[${BF_SCOPE}]`);
|
|
739
|
+
if (!nearestScope || !isInCommentScopeRange(nearestScope, commentInfo.commentNode))
|
|
740
|
+
return candidate;
|
|
741
|
+
}
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
715
744
|
function findInPortals(scopeId, selector) {
|
|
716
745
|
const portals = document.querySelectorAll(`[${BF_PORTAL_OWNER}="${scopeId}"]`);
|
|
717
746
|
for (const portal of portals) {
|
|
@@ -746,7 +775,14 @@ function qsa(el, selector) {
|
|
|
746
775
|
}
|
|
747
776
|
if (el.matches(selector))
|
|
748
777
|
return el;
|
|
749
|
-
|
|
778
|
+
const first = el.querySelector(selector);
|
|
779
|
+
if (!first || !isInsideNestedChildScope(first, el))
|
|
780
|
+
return first;
|
|
781
|
+
for (const candidate of el.querySelectorAll(selector)) {
|
|
782
|
+
if (candidate !== first && !isInsideNestedChildScope(candidate, el))
|
|
783
|
+
return candidate;
|
|
784
|
+
}
|
|
785
|
+
return null;
|
|
750
786
|
}
|
|
751
787
|
function splitTopLevelCommas(selector) {
|
|
752
788
|
const out = [];
|
|
@@ -2531,7 +2567,7 @@ function insert(scope, id, conditionFn, whenTrue, whenFalse, bfId) {
|
|
|
2531
2567
|
}
|
|
2532
2568
|
function autoFocusConditionalElement(region, id) {
|
|
2533
2569
|
requestAnimationFrame(() => {
|
|
2534
|
-
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : region.bindScope
|
|
2570
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : findCondTarget(region.bindScope, `[${BF_COND}="${id}"]`);
|
|
2535
2571
|
if (condEl) {
|
|
2536
2572
|
const autofocusEl = condEl.matches("[autofocus]") ? condEl : condEl.querySelector("[autofocus]");
|
|
2537
2573
|
if (autofocusEl && typeof autofocusEl.focus === "function") {
|
|
@@ -2572,15 +2608,14 @@ function updateFragmentConditional(region, id, result) {
|
|
|
2572
2608
|
if (region.anchor) {
|
|
2573
2609
|
startComment = findCondStartInRange(region.anchor, id);
|
|
2574
2610
|
} else {
|
|
2575
|
-
const
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
startComment = walker.currentNode;
|
|
2611
|
+
for (const comment of commentsInScope(scope)) {
|
|
2612
|
+
if (comment.nodeValue === startMarker) {
|
|
2613
|
+
startComment = comment;
|
|
2579
2614
|
break;
|
|
2580
2615
|
}
|
|
2581
2616
|
}
|
|
2582
2617
|
}
|
|
2583
|
-
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : scope
|
|
2618
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : findCondTarget(scope, `[${BF_COND}="${id}"]`);
|
|
2584
2619
|
const endMarker = `bf-cond-end:${id}`;
|
|
2585
2620
|
if (startComment) {
|
|
2586
2621
|
const nodesToRemove = [];
|
|
@@ -2620,7 +2655,7 @@ function updateFragmentConditional(region, id, result) {
|
|
|
2620
2655
|
}
|
|
2621
2656
|
}
|
|
2622
2657
|
function updateElementConditional(region, id, result) {
|
|
2623
|
-
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : region.bindScope
|
|
2658
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : findCondTarget(region.bindScope, `[${BF_COND}="${id}"]`);
|
|
2624
2659
|
if (!condEl)
|
|
2625
2660
|
return;
|
|
2626
2661
|
const { html, slots } = result;
|
package/dist/runtime/query.d.ts
CHANGED
|
@@ -30,11 +30,43 @@ export declare function findScope(name: string, idx: number, parent: Element | D
|
|
|
30
30
|
* @returns The matching element or null
|
|
31
31
|
*/
|
|
32
32
|
export declare function find(scope: Element | null, selector: string, ignoreScope?: boolean): Element | null;
|
|
33
|
+
/**
|
|
34
|
+
* Find a conditional's own target (a `bf-c="id"` element, for `insert.ts`)
|
|
35
|
+
* within `scope`'s content range.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately narrower than `find()`: for a regular (non-comment) scope,
|
|
38
|
+
* this is a **plain, unfiltered** `scope.querySelector(selector)` — not
|
|
39
|
+
* `find()`'s `belongsToScope`-gated search. `belongsToScope` accepts a
|
|
40
|
+
* candidate only if `candidate.closest('[bf-s]')` is *exactly* `scope` —
|
|
41
|
+
* which can never hold when `scope` itself carries no `bf-s`. That's the
|
|
42
|
+
* common case for `insert()`'s `region.bindScope`: a `mapArray` loop item's
|
|
43
|
+
* cloned template root (e.g. a `.comment-item` `<div>`) is keyed by
|
|
44
|
+
* `data-key` for reconciliation, not given its own `bf-s`, so any `bf-c`
|
|
45
|
+
* conditional inside that item would be rejected outright by
|
|
46
|
+
* `belongsToScope` — not because of any nested-child-scope subtlety, just
|
|
47
|
+
* because `scope` itself isn't `bf-s`-addressable. Using `find()` here
|
|
48
|
+
* previously broke exactly that shape (piconic-ai/barefootjs#2313's first
|
|
49
|
+
* attempt, caught by the `site/ui` e2e suite's `SocialThreadDemo`
|
|
50
|
+
* comment-editing test — a per-comment `editing` conditional inside a
|
|
51
|
+
* `sortedComments().map(...)` loop item with no `bf-s` of its own).
|
|
52
|
+
*
|
|
53
|
+
* A comment-scope proxy (fragment-root component) still gets the
|
|
54
|
+
* comment-range-bounded, nested-fragment-excluding search — the same rule
|
|
55
|
+
* `find()`'s comment branch uses — since that IS what's needed to walk past
|
|
56
|
+
* a fragment-root's own top-level siblings correctly (#2312).
|
|
57
|
+
*/
|
|
58
|
+
export declare function findCondTarget(scope: Element, selector: string): Element | null;
|
|
33
59
|
/**
|
|
34
60
|
* Find an element matching a selector, checking the element itself first,
|
|
35
61
|
* then its descendants. Unlike querySelector() which only searches descendants,
|
|
36
62
|
* this also matches the root element.
|
|
37
63
|
*
|
|
64
|
+
* Descendant candidates that fall inside a nested child component's own
|
|
65
|
+
* scope are skipped (`isInsideNestedChildScope`) — compiler slot IDs
|
|
66
|
+
* (`bf="sN"`) are assigned independently per component file, so a slot
|
|
67
|
+
* number can coincidentally collide between `el`'s own template and a
|
|
68
|
+
* child component mounted somewhere inside it (#2316).
|
|
69
|
+
*
|
|
38
70
|
* Used by compiler-generated code for event binding and attribute updates
|
|
39
71
|
* on loop items where the target may be the loop item's root element itself.
|
|
40
72
|
*/
|
|
@@ -98,6 +130,22 @@ export declare function $c(scope: Element | null, ...ids: string[]): (Element |
|
|
|
98
130
|
* walker-owned root fragments.
|
|
99
131
|
*/
|
|
100
132
|
export declare function findCommentChildScope(scope: Element, parentIds: readonly string[], slotId: string): Element | null;
|
|
133
|
+
/**
|
|
134
|
+
* Enumerate comment nodes in a scope's DOM range: the comment-scope
|
|
135
|
+
* sibling range for comment-anchored scopes, the element subtree
|
|
136
|
+
* otherwise. Mirrors candidatesInScope's notion of "where the scope's
|
|
137
|
+
* content lives", but yields comments instead of elements.
|
|
138
|
+
*
|
|
139
|
+
* Exported for `insert.ts`'s branch-swap path (`updateFragmentConditional`),
|
|
140
|
+
* which otherwise walked `scope`'s own descendants with a bare
|
|
141
|
+
* `document.createTreeWalker` — never finding a conditional's
|
|
142
|
+
* `bf-cond-start:`/`bf-cond-end:` markers when they sit as a *sibling* of
|
|
143
|
+
* the scope's comment-scope proxy rather than nested inside it (true for
|
|
144
|
+
* any fragment-root component's own top-level conditional, since the proxy
|
|
145
|
+
* is one specific top-level element and the conditional's markers may be
|
|
146
|
+
* others). See piconic-ai/sora's `ListSidebar` for the real-world repro.
|
|
147
|
+
*/
|
|
148
|
+
export declare function commentsInScope(scope: Element): Generator<Comment>;
|
|
101
149
|
/**
|
|
102
150
|
* Find Text nodes for reactive text expressions marked by comment nodes.
|
|
103
151
|
* Expects marker format: <!--bf:sX-->text<!--/-->
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/runtime/query.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,sFAAsF;AACtF,eAAO,MAAM,SAAS,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAGJ,CAAA;AAqCnC;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,EACjC,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,GAAG,IAAI,CAoDhB;
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/runtime/query.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,sFAAsF;AACtF,eAAO,MAAM,SAAS,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAGJ,CAAA;AAqCnC;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,EACjC,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,GAAG,IAAI,CAoDhB;AA2ND;;;;;;;;;;GAUG;AACH,wBAAgB,IAAI,CAClB,KAAK,EAAE,OAAO,GAAG,IAAI,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,OAAO,GACpB,OAAO,GAAG,IAAI,CAiChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAW/E;AAuBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,GAAG,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAsCxE;AA+CD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAU9E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,EAAE,CAO1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAO7E;AAED;;;;;GAKG;AACH,wBAAgB,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAE9E;AAkDD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,MAAM,EAAE,MAAM,GACb,OAAO,GAAG,IAAI,CAehB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAiB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC,OAAO,CAAC,CAkBnE;AAyDD;;;;;;;GAOG;AACH,wBAAgB,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,EAAE,GAAG,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAkC3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAS3D;AAED,OAAO,EAAE,oBAAoB,IAAI,MAAM,EAAE,CAAA"}
|
|
@@ -1064,6 +1064,21 @@ function isInsideNestedCommentScope(element, scope) {
|
|
|
1064
1064
|
}
|
|
1065
1065
|
return false;
|
|
1066
1066
|
}
|
|
1067
|
+
function isInsideNestedChildScope(element, root) {
|
|
1068
|
+
let current = element;
|
|
1069
|
+
while (current !== root) {
|
|
1070
|
+
const parent = current.parentNode;
|
|
1071
|
+
if (!parent)
|
|
1072
|
+
return false;
|
|
1073
|
+
if (parent !== root && parent.nodeType === Node.ELEMENT_NODE && parent.hasAttribute(BF_SCOPE)) {
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
if (isTopLevelCommentScopeNode(current))
|
|
1077
|
+
return true;
|
|
1078
|
+
current = parent;
|
|
1079
|
+
}
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1067
1082
|
function isTopLevelCommentScopeNode(node) {
|
|
1068
1083
|
const parent = node.parentNode;
|
|
1069
1084
|
if (!parent)
|
|
@@ -1120,6 +1135,20 @@ function find(scope, selector, ignoreScope) {
|
|
|
1120
1135
|
return findInPortals(scopeId, selector);
|
|
1121
1136
|
return null;
|
|
1122
1137
|
}
|
|
1138
|
+
function findCondTarget(scope, selector) {
|
|
1139
|
+
const commentInfo = commentScopeRegistry.get(scope);
|
|
1140
|
+
if (!commentInfo) {
|
|
1141
|
+
return scope.querySelector(selector);
|
|
1142
|
+
}
|
|
1143
|
+
for (const candidate of candidatesInScope(scope, selector)) {
|
|
1144
|
+
if (candidate.parentElement === commentInfo.commentNode.parentElement)
|
|
1145
|
+
return candidate;
|
|
1146
|
+
const nearestScope = candidate.closest(`[${BF_SCOPE}]`);
|
|
1147
|
+
if (!nearestScope || !isInCommentScopeRange(nearestScope, commentInfo.commentNode))
|
|
1148
|
+
return candidate;
|
|
1149
|
+
}
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1123
1152
|
function findInPortals(scopeId, selector) {
|
|
1124
1153
|
const portals = document.querySelectorAll(`[${BF_PORTAL_OWNER}="${scopeId}"]`);
|
|
1125
1154
|
for (const portal of portals) {
|
|
@@ -1154,7 +1183,14 @@ function qsa(el, selector) {
|
|
|
1154
1183
|
}
|
|
1155
1184
|
if (el.matches(selector))
|
|
1156
1185
|
return el;
|
|
1157
|
-
|
|
1186
|
+
const first = el.querySelector(selector);
|
|
1187
|
+
if (!first || !isInsideNestedChildScope(first, el))
|
|
1188
|
+
return first;
|
|
1189
|
+
for (const candidate of el.querySelectorAll(selector)) {
|
|
1190
|
+
if (candidate !== first && !isInsideNestedChildScope(candidate, el))
|
|
1191
|
+
return candidate;
|
|
1192
|
+
}
|
|
1193
|
+
return null;
|
|
1158
1194
|
}
|
|
1159
1195
|
function splitTopLevelCommas(selector) {
|
|
1160
1196
|
const out = [];
|
|
@@ -2932,7 +2968,7 @@ function insert(scope, id, conditionFn, whenTrue, whenFalse, bfId) {
|
|
|
2932
2968
|
}
|
|
2933
2969
|
function autoFocusConditionalElement(region, id) {
|
|
2934
2970
|
requestAnimationFrame(() => {
|
|
2935
|
-
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : region.bindScope
|
|
2971
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : findCondTarget(region.bindScope, `[${BF_COND}="${id}"]`);
|
|
2936
2972
|
if (condEl) {
|
|
2937
2973
|
const autofocusEl = condEl.matches("[autofocus]") ? condEl : condEl.querySelector("[autofocus]");
|
|
2938
2974
|
if (autofocusEl && typeof autofocusEl.focus === "function") {
|
|
@@ -2973,15 +3009,14 @@ function updateFragmentConditional(region, id, result) {
|
|
|
2973
3009
|
if (region.anchor) {
|
|
2974
3010
|
startComment = findCondStartInRange(region.anchor, id);
|
|
2975
3011
|
} else {
|
|
2976
|
-
const
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
startComment = walker.currentNode;
|
|
3012
|
+
for (const comment of commentsInScope(scope)) {
|
|
3013
|
+
if (comment.nodeValue === startMarker) {
|
|
3014
|
+
startComment = comment;
|
|
2980
3015
|
break;
|
|
2981
3016
|
}
|
|
2982
3017
|
}
|
|
2983
3018
|
}
|
|
2984
|
-
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : scope
|
|
3019
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : findCondTarget(scope, `[${BF_COND}="${id}"]`);
|
|
2985
3020
|
const endMarker = `bf-cond-end:${id}`;
|
|
2986
3021
|
if (startComment) {
|
|
2987
3022
|
const nodesToRemove = [];
|
|
@@ -3021,7 +3056,7 @@ function updateFragmentConditional(region, id, result) {
|
|
|
3021
3056
|
}
|
|
3022
3057
|
}
|
|
3023
3058
|
function updateElementConditional(region, id, result) {
|
|
3024
|
-
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : region.bindScope
|
|
3059
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : findCondTarget(region.bindScope, `[${BF_COND}="${id}"]`);
|
|
3025
3060
|
if (!condEl)
|
|
3026
3061
|
return;
|
|
3027
3062
|
const { html, slots } = result;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/client",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.4",
|
|
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.21.
|
|
58
|
+
"@barefootjs/shared": "0.21.4"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@barefootjs/jsx": ">=0.2.0"
|
package/src/runtime/insert.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { createEffect, untrack } from '@barefootjs/client/reactive'
|
|
10
|
-
import { find } from './query.ts'
|
|
10
|
+
import { find, findCondTarget, commentsInScope } from './query.ts'
|
|
11
11
|
import { setParentScopeId, parseHTML } from './component.ts'
|
|
12
12
|
import { commentScopeRegistry, getCommentScopeBoundary } from './scope.ts'
|
|
13
13
|
import { BF_COND, BF_SCOPE, BF_LOOP_ITEM } from '@barefootjs/shared'
|
|
@@ -342,7 +342,7 @@ function autoFocusConditionalElement(region: CondRegion, id: string): void {
|
|
|
342
342
|
requestAnimationFrame(() => {
|
|
343
343
|
const condEl = region.anchor
|
|
344
344
|
? findCondElInRange(region.anchor, id)
|
|
345
|
-
: region.bindScope
|
|
345
|
+
: findCondTarget(region.bindScope, `[${BF_COND}="${id}"]`)
|
|
346
346
|
if (condEl) {
|
|
347
347
|
const autofocusEl = condEl.matches('[autofocus]')
|
|
348
348
|
? condEl
|
|
@@ -406,10 +406,15 @@ function updateFragmentConditional(region: CondRegion, id: string, result: Branc
|
|
|
406
406
|
if (region.anchor) {
|
|
407
407
|
startComment = findCondStartInRange(region.anchor, id)
|
|
408
408
|
} else {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
409
|
+
// commentsInScope is comment-scope-aware: for a fragment-root
|
|
410
|
+
// component's scope (a comment-scope proxy element), it walks the
|
|
411
|
+
// proxy's *sibling* range rather than only `scope`'s own descendants —
|
|
412
|
+
// required when this conditional's markers are themselves a top-level
|
|
413
|
+
// sibling of the proxy, not nested inside it (a bare TreeWalker(scope)
|
|
414
|
+
// never finds them, silently freezing the branch after first render).
|
|
415
|
+
for (const comment of commentsInScope(scope)) {
|
|
416
|
+
if (comment.nodeValue === startMarker) {
|
|
417
|
+
startComment = comment
|
|
413
418
|
break
|
|
414
419
|
}
|
|
415
420
|
}
|
|
@@ -417,7 +422,7 @@ function updateFragmentConditional(region: CondRegion, id: string, result: Branc
|
|
|
417
422
|
|
|
418
423
|
const condEl = region.anchor
|
|
419
424
|
? findCondElInRange(region.anchor, id)
|
|
420
|
-
: scope
|
|
425
|
+
: findCondTarget(scope, `[${BF_COND}="${id}"]`)
|
|
421
426
|
|
|
422
427
|
const endMarker = `bf-cond-end:${id}`
|
|
423
428
|
|
|
@@ -486,9 +491,15 @@ function updateFragmentConditional(region: CondRegion, id: string, result: Branc
|
|
|
486
491
|
* Update element conditional (single element with bf-c)
|
|
487
492
|
*/
|
|
488
493
|
function updateElementConditional(region: CondRegion, id: string, result: BranchTemplateResult): void {
|
|
494
|
+
// findCondTarget, not find(): for a comment-scope proxy it resolves a
|
|
495
|
+
// top-level sibling conditional the same way first hydration does, but
|
|
496
|
+
// for a regular scope it's a plain scope.querySelector(...) — not
|
|
497
|
+
// find()'s belongsToScope-gated search, which requires scope itself to
|
|
498
|
+
// carry bf-s (a mapArray loop item's cloned root usually doesn't; see
|
|
499
|
+
// findCondTarget's doc comment in query.ts).
|
|
489
500
|
const condEl = region.anchor
|
|
490
501
|
? findCondElInRange(region.anchor, id)
|
|
491
|
-
: region.bindScope
|
|
502
|
+
: findCondTarget(region.bindScope, `[${BF_COND}="${id}"]`)
|
|
492
503
|
if (!condEl) return
|
|
493
504
|
|
|
494
505
|
const { html, slots } = result
|
package/src/runtime/query.ts
CHANGED
|
@@ -251,6 +251,48 @@ function isInsideNestedCommentScope(element: Element, scope: Element): boolean {
|
|
|
251
251
|
return false
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
/**
|
|
255
|
+
* True if `element` sits inside a nested child component's own scope
|
|
256
|
+
* boundary somewhere on the path up to `root` — a plain `[bf-s]` element
|
|
257
|
+
* (a regular child component's root) or a fragment-rooted child's
|
|
258
|
+
* comment-scope range mounted as a sibling along that path.
|
|
259
|
+
*
|
|
260
|
+
* Generalizes `isInsideNestedCommentScope` (which only catches the
|
|
261
|
+
* comment-scope case) to also catch plain `bf-s` boundaries, for `qsa()`
|
|
262
|
+
* — the loose, unfiltered sibling of `find()`'s `belongsToScope`-gated
|
|
263
|
+
* search. Unlike `belongsToScope`, this walks up to `root` by object
|
|
264
|
+
* identity rather than requiring `root` itself to carry `bf-s`: `qsa()`'s
|
|
265
|
+
* roots include `insert()`'s whole-component branch scope (which does
|
|
266
|
+
* carry `bf-s`) but also loop-item template clones (which normally don't),
|
|
267
|
+
* so a `.closest('[bf-s]') === root` check can't be reused here.
|
|
268
|
+
*
|
|
269
|
+
* Without this, a slot number that happens to collide between a component
|
|
270
|
+
* and a nested child mounted earlier in its DOM (compiler slot IDs are
|
|
271
|
+
* assigned independently per component file, so collisions are expected)
|
|
272
|
+
* makes `qsa()` return the child's element instead of the component's own.
|
|
273
|
+
*
|
|
274
|
+
* Deliberately does NOT reject `element` itself for carrying `bf-s`
|
|
275
|
+
* (unlike `belongsToScope`'s own-scope check) — `qsa()` doubles as the
|
|
276
|
+
* lookup for a nested child component's *own* scope root (e.g. compiled
|
|
277
|
+
* `qsa(parentScope, '[bf-h="X"][bf-m="sN"], [bf-s$="_sN"]')` calls feeding
|
|
278
|
+
* `initChild()`), and that target legitimately carries `bf-s`. Only an
|
|
279
|
+
* *intermediate* `bf-s` between `element` and `root` — i.e. `element`
|
|
280
|
+
* nested inside some other, unrelated child scope — disqualifies it.
|
|
281
|
+
*/
|
|
282
|
+
function isInsideNestedChildScope(element: Element, root: Element): boolean {
|
|
283
|
+
let current: Node = element
|
|
284
|
+
while (current !== root) {
|
|
285
|
+
const parent: Node | null = current.parentNode
|
|
286
|
+
if (!parent) return false
|
|
287
|
+
if (parent !== root && parent.nodeType === Node.ELEMENT_NODE && (parent as Element).hasAttribute(BF_SCOPE)) {
|
|
288
|
+
return true
|
|
289
|
+
}
|
|
290
|
+
if (isTopLevelCommentScopeNode(current)) return true
|
|
291
|
+
current = parent
|
|
292
|
+
}
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
|
|
254
296
|
/**
|
|
255
297
|
* True if `node` falls inside a `<!--bf-scope:...-->` … `<!--bf-/scope:...-->`
|
|
256
298
|
* range among its own siblings — i.e. `node` is (or is inside) a top-level
|
|
@@ -345,6 +387,44 @@ export function find(
|
|
|
345
387
|
return null
|
|
346
388
|
}
|
|
347
389
|
|
|
390
|
+
/**
|
|
391
|
+
* Find a conditional's own target (a `bf-c="id"` element, for `insert.ts`)
|
|
392
|
+
* within `scope`'s content range.
|
|
393
|
+
*
|
|
394
|
+
* Deliberately narrower than `find()`: for a regular (non-comment) scope,
|
|
395
|
+
* this is a **plain, unfiltered** `scope.querySelector(selector)` — not
|
|
396
|
+
* `find()`'s `belongsToScope`-gated search. `belongsToScope` accepts a
|
|
397
|
+
* candidate only if `candidate.closest('[bf-s]')` is *exactly* `scope` —
|
|
398
|
+
* which can never hold when `scope` itself carries no `bf-s`. That's the
|
|
399
|
+
* common case for `insert()`'s `region.bindScope`: a `mapArray` loop item's
|
|
400
|
+
* cloned template root (e.g. a `.comment-item` `<div>`) is keyed by
|
|
401
|
+
* `data-key` for reconciliation, not given its own `bf-s`, so any `bf-c`
|
|
402
|
+
* conditional inside that item would be rejected outright by
|
|
403
|
+
* `belongsToScope` — not because of any nested-child-scope subtlety, just
|
|
404
|
+
* because `scope` itself isn't `bf-s`-addressable. Using `find()` here
|
|
405
|
+
* previously broke exactly that shape (piconic-ai/barefootjs#2313's first
|
|
406
|
+
* attempt, caught by the `site/ui` e2e suite's `SocialThreadDemo`
|
|
407
|
+
* comment-editing test — a per-comment `editing` conditional inside a
|
|
408
|
+
* `sortedComments().map(...)` loop item with no `bf-s` of its own).
|
|
409
|
+
*
|
|
410
|
+
* A comment-scope proxy (fragment-root component) still gets the
|
|
411
|
+
* comment-range-bounded, nested-fragment-excluding search — the same rule
|
|
412
|
+
* `find()`'s comment branch uses — since that IS what's needed to walk past
|
|
413
|
+
* a fragment-root's own top-level siblings correctly (#2312).
|
|
414
|
+
*/
|
|
415
|
+
export function findCondTarget(scope: Element, selector: string): Element | null {
|
|
416
|
+
const commentInfo = commentScopeRegistry.get(scope)
|
|
417
|
+
if (!commentInfo) {
|
|
418
|
+
return scope.querySelector(selector)
|
|
419
|
+
}
|
|
420
|
+
for (const candidate of candidatesInScope(scope, selector)) {
|
|
421
|
+
if (candidate.parentElement === commentInfo.commentNode.parentElement) return candidate
|
|
422
|
+
const nearestScope = candidate.closest(`[${BF_SCOPE}]`)
|
|
423
|
+
if (!nearestScope || !isInCommentScopeRange(nearestScope, commentInfo.commentNode)) return candidate
|
|
424
|
+
}
|
|
425
|
+
return null
|
|
426
|
+
}
|
|
427
|
+
|
|
348
428
|
/**
|
|
349
429
|
* Search in portals owned by a scope.
|
|
350
430
|
*/
|
|
@@ -371,6 +451,12 @@ function findInPortals(scopeId: string, selector: string): Element | null {
|
|
|
371
451
|
* then its descendants. Unlike querySelector() which only searches descendants,
|
|
372
452
|
* this also matches the root element.
|
|
373
453
|
*
|
|
454
|
+
* Descendant candidates that fall inside a nested child component's own
|
|
455
|
+
* scope are skipped (`isInsideNestedChildScope`) — compiler slot IDs
|
|
456
|
+
* (`bf="sN"`) are assigned independently per component file, so a slot
|
|
457
|
+
* number can coincidentally collide between `el`'s own template and a
|
|
458
|
+
* child component mounted somewhere inside it (#2316).
|
|
459
|
+
*
|
|
374
460
|
* Used by compiler-generated code for event binding and attribute updates
|
|
375
461
|
* on loop items where the target may be the loop item's root element itself.
|
|
376
462
|
*/
|
|
@@ -399,7 +485,19 @@ export function qsa(el: Element | null, selector: string): Element | null {
|
|
|
399
485
|
return qsaChildScope(el, selector)
|
|
400
486
|
}
|
|
401
487
|
if (el.matches(selector)) return el
|
|
402
|
-
|
|
488
|
+
|
|
489
|
+
// Fast path: the overwhelmingly common case has no nested-child-scope
|
|
490
|
+
// collision, so a single querySelector() (matching the old behavior)
|
|
491
|
+
// resolves it without the cost of enumerating every match. Only fall
|
|
492
|
+
// back to the full querySelectorAll() scan — needed to skip past a
|
|
493
|
+
// rejected candidate to the next DOM-order match — when the first hit
|
|
494
|
+
// actually turns out to belong to a nested child scope.
|
|
495
|
+
const first = el.querySelector(selector)
|
|
496
|
+
if (!first || !isInsideNestedChildScope(first, el)) return first
|
|
497
|
+
for (const candidate of el.querySelectorAll(selector)) {
|
|
498
|
+
if (candidate !== first && !isInsideNestedChildScope(candidate, el)) return candidate
|
|
499
|
+
}
|
|
500
|
+
return null
|
|
403
501
|
}
|
|
404
502
|
|
|
405
503
|
/** Split a CSS selector list on top-level commas, ignoring commas inside
|
|
@@ -609,8 +707,17 @@ export function findCommentChildScope(
|
|
|
609
707
|
* sibling range for comment-anchored scopes, the element subtree
|
|
610
708
|
* otherwise. Mirrors candidatesInScope's notion of "where the scope's
|
|
611
709
|
* content lives", but yields comments instead of elements.
|
|
710
|
+
*
|
|
711
|
+
* Exported for `insert.ts`'s branch-swap path (`updateFragmentConditional`),
|
|
712
|
+
* which otherwise walked `scope`'s own descendants with a bare
|
|
713
|
+
* `document.createTreeWalker` — never finding a conditional's
|
|
714
|
+
* `bf-cond-start:`/`bf-cond-end:` markers when they sit as a *sibling* of
|
|
715
|
+
* the scope's comment-scope proxy rather than nested inside it (true for
|
|
716
|
+
* any fragment-root component's own top-level conditional, since the proxy
|
|
717
|
+
* is one specific top-level element and the conditional's markers may be
|
|
718
|
+
* others). See piconic-ai/sora's `ListSidebar` for the real-world repro.
|
|
612
719
|
*/
|
|
613
|
-
function* commentsInScope(scope: Element): Generator<Comment> {
|
|
720
|
+
export function* commentsInScope(scope: Element): Generator<Comment> {
|
|
614
721
|
const commentInfo = commentScopeRegistry.get(scope)
|
|
615
722
|
|
|
616
723
|
if (commentInfo) {
|