@barefootjs/client 0.21.0 → 0.21.3
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 +57 -9
- package/dist/runtime/query.d.ts +42 -0
- package/dist/runtime/query.d.ts.map +1 -1
- package/dist/runtime/standalone.js +57 -9
- package/package.json +2 -2
- package/src/runtime/insert.ts +19 -8
- package/src/runtime/query.ts +111 -6
package/dist/runtime/index.js
CHANGED
|
@@ -640,7 +640,41 @@ function belongsToScope(element, scope) {
|
|
|
640
640
|
if (element.getAttribute(BF_SCOPE))
|
|
641
641
|
return false;
|
|
642
642
|
const nearestScope = element.closest(`[${BF_SCOPE}]`);
|
|
643
|
-
|
|
643
|
+
if (nearestScope !== scope)
|
|
644
|
+
return false;
|
|
645
|
+
return !isInsideNestedCommentScope(element, scope);
|
|
646
|
+
}
|
|
647
|
+
function isInsideNestedCommentScope(element, scope) {
|
|
648
|
+
let current = element;
|
|
649
|
+
while (current !== scope) {
|
|
650
|
+
const parent = current.parentNode;
|
|
651
|
+
if (!parent)
|
|
652
|
+
return false;
|
|
653
|
+
if (isTopLevelCommentScopeNode(current))
|
|
654
|
+
return true;
|
|
655
|
+
current = parent;
|
|
656
|
+
}
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
function isTopLevelCommentScopeNode(node) {
|
|
660
|
+
const parent = node.parentNode;
|
|
661
|
+
if (!parent)
|
|
662
|
+
return false;
|
|
663
|
+
let depth = 0;
|
|
664
|
+
let sib = parent.firstChild;
|
|
665
|
+
while (sib) {
|
|
666
|
+
if (sib === node)
|
|
667
|
+
return depth > 0;
|
|
668
|
+
if (sib.nodeType === Node.COMMENT_NODE) {
|
|
669
|
+
const value = sib.nodeValue ?? "";
|
|
670
|
+
if (value.startsWith(BF_SCOPE_COMMENT_PREFIX))
|
|
671
|
+
depth++;
|
|
672
|
+
else if (value.startsWith(BF_SCOPE_COMMENT_END_PREFIX) && depth > 0)
|
|
673
|
+
depth--;
|
|
674
|
+
}
|
|
675
|
+
sib = sib.nextSibling;
|
|
676
|
+
}
|
|
677
|
+
return false;
|
|
644
678
|
}
|
|
645
679
|
function isInCommentScopeRange(element, commentNode) {
|
|
646
680
|
const boundary = getCommentScopeBoundary(commentNode);
|
|
@@ -665,7 +699,8 @@ function find(scope, selector, ignoreScope) {
|
|
|
665
699
|
if (commentInfo) {
|
|
666
700
|
if (candidate.parentElement === commentInfo.commentNode.parentElement)
|
|
667
701
|
return candidate;
|
|
668
|
-
|
|
702
|
+
const nearestScope = candidate.closest(`[${BF_SCOPE}]`);
|
|
703
|
+
if (!nearestScope || !isInCommentScopeRange(nearestScope, commentInfo.commentNode))
|
|
669
704
|
return candidate;
|
|
670
705
|
} else {
|
|
671
706
|
if (belongsToScope(candidate, scope))
|
|
@@ -677,6 +712,20 @@ function find(scope, selector, ignoreScope) {
|
|
|
677
712
|
return findInPortals(scopeId, selector);
|
|
678
713
|
return null;
|
|
679
714
|
}
|
|
715
|
+
function findCondTarget(scope, selector) {
|
|
716
|
+
const commentInfo = commentScopeRegistry.get(scope);
|
|
717
|
+
if (!commentInfo) {
|
|
718
|
+
return scope.querySelector(selector);
|
|
719
|
+
}
|
|
720
|
+
for (const candidate of candidatesInScope(scope, selector)) {
|
|
721
|
+
if (candidate.parentElement === commentInfo.commentNode.parentElement)
|
|
722
|
+
return candidate;
|
|
723
|
+
const nearestScope = candidate.closest(`[${BF_SCOPE}]`);
|
|
724
|
+
if (!nearestScope || !isInCommentScopeRange(nearestScope, commentInfo.commentNode))
|
|
725
|
+
return candidate;
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
680
729
|
function findInPortals(scopeId, selector) {
|
|
681
730
|
const portals = document.querySelectorAll(`[${BF_PORTAL_OWNER}="${scopeId}"]`);
|
|
682
731
|
for (const portal of portals) {
|
|
@@ -2496,7 +2545,7 @@ function insert(scope, id, conditionFn, whenTrue, whenFalse, bfId) {
|
|
|
2496
2545
|
}
|
|
2497
2546
|
function autoFocusConditionalElement(region, id) {
|
|
2498
2547
|
requestAnimationFrame(() => {
|
|
2499
|
-
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : region.bindScope
|
|
2548
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : findCondTarget(region.bindScope, `[${BF_COND}="${id}"]`);
|
|
2500
2549
|
if (condEl) {
|
|
2501
2550
|
const autofocusEl = condEl.matches("[autofocus]") ? condEl : condEl.querySelector("[autofocus]");
|
|
2502
2551
|
if (autofocusEl && typeof autofocusEl.focus === "function") {
|
|
@@ -2537,15 +2586,14 @@ function updateFragmentConditional(region, id, result) {
|
|
|
2537
2586
|
if (region.anchor) {
|
|
2538
2587
|
startComment = findCondStartInRange(region.anchor, id);
|
|
2539
2588
|
} else {
|
|
2540
|
-
const
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
startComment = walker.currentNode;
|
|
2589
|
+
for (const comment of commentsInScope(scope)) {
|
|
2590
|
+
if (comment.nodeValue === startMarker) {
|
|
2591
|
+
startComment = comment;
|
|
2544
2592
|
break;
|
|
2545
2593
|
}
|
|
2546
2594
|
}
|
|
2547
2595
|
}
|
|
2548
|
-
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : scope
|
|
2596
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : findCondTarget(scope, `[${BF_COND}="${id}"]`);
|
|
2549
2597
|
const endMarker = `bf-cond-end:${id}`;
|
|
2550
2598
|
if (startComment) {
|
|
2551
2599
|
const nodesToRemove = [];
|
|
@@ -2585,7 +2633,7 @@ function updateFragmentConditional(region, id, result) {
|
|
|
2585
2633
|
}
|
|
2586
2634
|
}
|
|
2587
2635
|
function updateElementConditional(region, id, result) {
|
|
2588
|
-
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : region.bindScope
|
|
2636
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : findCondTarget(region.bindScope, `[${BF_COND}="${id}"]`);
|
|
2589
2637
|
if (!condEl)
|
|
2590
2638
|
return;
|
|
2591
2639
|
const { html, slots } = result;
|
package/dist/runtime/query.d.ts
CHANGED
|
@@ -30,6 +30,32 @@ 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,
|
|
@@ -98,6 +124,22 @@ export declare function $c(scope: Element | null, ...ids: string[]): (Element |
|
|
|
98
124
|
* walker-owned root fragments.
|
|
99
125
|
*/
|
|
100
126
|
export declare function findCommentChildScope(scope: Element, parentIds: readonly string[], slotId: string): Element | null;
|
|
127
|
+
/**
|
|
128
|
+
* Enumerate comment nodes in a scope's DOM range: the comment-scope
|
|
129
|
+
* sibling range for comment-anchored scopes, the element subtree
|
|
130
|
+
* otherwise. Mirrors candidatesInScope's notion of "where the scope's
|
|
131
|
+
* content lives", but yields comments instead of elements.
|
|
132
|
+
*
|
|
133
|
+
* Exported for `insert.ts`'s branch-swap path (`updateFragmentConditional`),
|
|
134
|
+
* which otherwise walked `scope`'s own descendants with a bare
|
|
135
|
+
* `document.createTreeWalker` — never finding a conditional's
|
|
136
|
+
* `bf-cond-start:`/`bf-cond-end:` markers when they sit as a *sibling* of
|
|
137
|
+
* the scope's comment-scope proxy rather than nested inside it (true for
|
|
138
|
+
* any fragment-root component's own top-level conditional, since the proxy
|
|
139
|
+
* is one specific top-level element and the conditional's markers may be
|
|
140
|
+
* others). See piconic-ai/sora's `ListSidebar` for the real-world repro.
|
|
141
|
+
*/
|
|
142
|
+
export declare function commentsInScope(scope: Element): Generator<Comment>;
|
|
101
143
|
/**
|
|
102
144
|
* Find Text nodes for reactive text expressions marked by comment nodes.
|
|
103
145
|
* 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;AAiLD;;;;;;;;;;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;;;;;;;GAOG;AACH,wBAAgB,GAAG,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CA0BxE;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"}
|
|
@@ -1048,7 +1048,41 @@ function belongsToScope(element, scope) {
|
|
|
1048
1048
|
if (element.getAttribute(BF_SCOPE))
|
|
1049
1049
|
return false;
|
|
1050
1050
|
const nearestScope = element.closest(`[${BF_SCOPE}]`);
|
|
1051
|
-
|
|
1051
|
+
if (nearestScope !== scope)
|
|
1052
|
+
return false;
|
|
1053
|
+
return !isInsideNestedCommentScope(element, scope);
|
|
1054
|
+
}
|
|
1055
|
+
function isInsideNestedCommentScope(element, scope) {
|
|
1056
|
+
let current = element;
|
|
1057
|
+
while (current !== scope) {
|
|
1058
|
+
const parent = current.parentNode;
|
|
1059
|
+
if (!parent)
|
|
1060
|
+
return false;
|
|
1061
|
+
if (isTopLevelCommentScopeNode(current))
|
|
1062
|
+
return true;
|
|
1063
|
+
current = parent;
|
|
1064
|
+
}
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
function isTopLevelCommentScopeNode(node) {
|
|
1068
|
+
const parent = node.parentNode;
|
|
1069
|
+
if (!parent)
|
|
1070
|
+
return false;
|
|
1071
|
+
let depth = 0;
|
|
1072
|
+
let sib = parent.firstChild;
|
|
1073
|
+
while (sib) {
|
|
1074
|
+
if (sib === node)
|
|
1075
|
+
return depth > 0;
|
|
1076
|
+
if (sib.nodeType === Node.COMMENT_NODE) {
|
|
1077
|
+
const value = sib.nodeValue ?? "";
|
|
1078
|
+
if (value.startsWith(BF_SCOPE_COMMENT_PREFIX))
|
|
1079
|
+
depth++;
|
|
1080
|
+
else if (value.startsWith(BF_SCOPE_COMMENT_END_PREFIX) && depth > 0)
|
|
1081
|
+
depth--;
|
|
1082
|
+
}
|
|
1083
|
+
sib = sib.nextSibling;
|
|
1084
|
+
}
|
|
1085
|
+
return false;
|
|
1052
1086
|
}
|
|
1053
1087
|
function isInCommentScopeRange(element, commentNode) {
|
|
1054
1088
|
const boundary = getCommentScopeBoundary(commentNode);
|
|
@@ -1073,7 +1107,8 @@ function find(scope, selector, ignoreScope) {
|
|
|
1073
1107
|
if (commentInfo) {
|
|
1074
1108
|
if (candidate.parentElement === commentInfo.commentNode.parentElement)
|
|
1075
1109
|
return candidate;
|
|
1076
|
-
|
|
1110
|
+
const nearestScope = candidate.closest(`[${BF_SCOPE}]`);
|
|
1111
|
+
if (!nearestScope || !isInCommentScopeRange(nearestScope, commentInfo.commentNode))
|
|
1077
1112
|
return candidate;
|
|
1078
1113
|
} else {
|
|
1079
1114
|
if (belongsToScope(candidate, scope))
|
|
@@ -1085,6 +1120,20 @@ function find(scope, selector, ignoreScope) {
|
|
|
1085
1120
|
return findInPortals(scopeId, selector);
|
|
1086
1121
|
return null;
|
|
1087
1122
|
}
|
|
1123
|
+
function findCondTarget(scope, selector) {
|
|
1124
|
+
const commentInfo = commentScopeRegistry.get(scope);
|
|
1125
|
+
if (!commentInfo) {
|
|
1126
|
+
return scope.querySelector(selector);
|
|
1127
|
+
}
|
|
1128
|
+
for (const candidate of candidatesInScope(scope, selector)) {
|
|
1129
|
+
if (candidate.parentElement === commentInfo.commentNode.parentElement)
|
|
1130
|
+
return candidate;
|
|
1131
|
+
const nearestScope = candidate.closest(`[${BF_SCOPE}]`);
|
|
1132
|
+
if (!nearestScope || !isInCommentScopeRange(nearestScope, commentInfo.commentNode))
|
|
1133
|
+
return candidate;
|
|
1134
|
+
}
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1088
1137
|
function findInPortals(scopeId, selector) {
|
|
1089
1138
|
const portals = document.querySelectorAll(`[${BF_PORTAL_OWNER}="${scopeId}"]`);
|
|
1090
1139
|
for (const portal of portals) {
|
|
@@ -2897,7 +2946,7 @@ function insert(scope, id, conditionFn, whenTrue, whenFalse, bfId) {
|
|
|
2897
2946
|
}
|
|
2898
2947
|
function autoFocusConditionalElement(region, id) {
|
|
2899
2948
|
requestAnimationFrame(() => {
|
|
2900
|
-
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : region.bindScope
|
|
2949
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : findCondTarget(region.bindScope, `[${BF_COND}="${id}"]`);
|
|
2901
2950
|
if (condEl) {
|
|
2902
2951
|
const autofocusEl = condEl.matches("[autofocus]") ? condEl : condEl.querySelector("[autofocus]");
|
|
2903
2952
|
if (autofocusEl && typeof autofocusEl.focus === "function") {
|
|
@@ -2938,15 +2987,14 @@ function updateFragmentConditional(region, id, result) {
|
|
|
2938
2987
|
if (region.anchor) {
|
|
2939
2988
|
startComment = findCondStartInRange(region.anchor, id);
|
|
2940
2989
|
} else {
|
|
2941
|
-
const
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
startComment = walker.currentNode;
|
|
2990
|
+
for (const comment of commentsInScope(scope)) {
|
|
2991
|
+
if (comment.nodeValue === startMarker) {
|
|
2992
|
+
startComment = comment;
|
|
2945
2993
|
break;
|
|
2946
2994
|
}
|
|
2947
2995
|
}
|
|
2948
2996
|
}
|
|
2949
|
-
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : scope
|
|
2997
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : findCondTarget(scope, `[${BF_COND}="${id}"]`);
|
|
2950
2998
|
const endMarker = `bf-cond-end:${id}`;
|
|
2951
2999
|
if (startComment) {
|
|
2952
3000
|
const nodesToRemove = [];
|
|
@@ -2986,7 +3034,7 @@ function updateFragmentConditional(region, id, result) {
|
|
|
2986
3034
|
}
|
|
2987
3035
|
}
|
|
2988
3036
|
function updateElementConditional(region, id, result) {
|
|
2989
|
-
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : region.bindScope
|
|
3037
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : findCondTarget(region.bindScope, `[${BF_COND}="${id}"]`);
|
|
2990
3038
|
if (!condEl)
|
|
2991
3039
|
return;
|
|
2992
3040
|
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.3",
|
|
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.3"
|
|
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
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { commentScopeRegistry, getCommentScopeBoundary } from './scope.ts'
|
|
10
10
|
import { hydratedScopes } from './hydration-state.ts'
|
|
11
|
-
import { BF_SCOPE, BF_SLOT, BF_PORTAL_OWNER, BF_PARENT_OWNED_PREFIX, BF_SCOPE_COMMENT_PREFIX } from '@barefootjs/shared'
|
|
11
|
+
import { BF_SCOPE, BF_SLOT, BF_PORTAL_OWNER, BF_PARENT_OWNED_PREFIX, BF_SCOPE_COMMENT_PREFIX, BF_SCOPE_COMMENT_END_PREFIX } from '@barefootjs/shared'
|
|
12
12
|
|
|
13
13
|
/** CSS attribute-value escape with a fallback for environments lacking CSS.escape. */
|
|
14
14
|
export const cssEscape: (s: string) => string =
|
|
@@ -222,7 +222,59 @@ function belongsToScope(element: Element, scope: Element): boolean {
|
|
|
222
222
|
|
|
223
223
|
// Check if nearest scope matches
|
|
224
224
|
const nearestScope = element.closest(`[${BF_SCOPE}]`)
|
|
225
|
-
|
|
225
|
+
if (nearestScope !== scope) return false
|
|
226
|
+
|
|
227
|
+
// A fragment-rooted child mounted between `element` and `scope` has no
|
|
228
|
+
// `bf-s` element of its own (#2302) — `closest()` walks straight past it
|
|
229
|
+
// to `scope`, so a slot search on `scope` can wrongly claim a descendant
|
|
230
|
+
// that actually belongs to that nested child's comment scope. Reject it.
|
|
231
|
+
return !isInsideNestedCommentScope(element, scope)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* True if some ancestor of `element` (strictly below `scope`) is a
|
|
236
|
+
* top-level node of a fragment-rooted child's `<!--bf-scope:...-->` range —
|
|
237
|
+
* i.e. `element` structurally belongs to a nested child component even
|
|
238
|
+
* though that child has no `bf-s`-attributed element for `.closest()` to
|
|
239
|
+
* stop at, and regardless of whether `commentScopeRegistry` has registered
|
|
240
|
+
* it yet (a parent's own slot lookups can run before its child-scope
|
|
241
|
+
* lookups register the child, #2302).
|
|
242
|
+
*/
|
|
243
|
+
function isInsideNestedCommentScope(element: Element, scope: Element): boolean {
|
|
244
|
+
let current: Node = element
|
|
245
|
+
while (current !== scope) {
|
|
246
|
+
const parent: Node | null = current.parentNode
|
|
247
|
+
if (!parent) return false
|
|
248
|
+
if (isTopLevelCommentScopeNode(current)) return true
|
|
249
|
+
current = parent
|
|
250
|
+
}
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* True if `node` falls inside a `<!--bf-scope:...-->` … `<!--bf-/scope:...-->`
|
|
256
|
+
* range among its own siblings — i.e. `node` is (or is inside) a top-level
|
|
257
|
+
* element of a fragment-rooted child mounted as `node`'s sibling. Single
|
|
258
|
+
* left-to-right pass over the sibling list with a nesting depth counter, so
|
|
259
|
+
* a nested child's own begin/end pair doesn't prematurely close an outer
|
|
260
|
+
* one. Missing end markers (older SSR output) leave depth un-decremented,
|
|
261
|
+
* matching `getCommentScopeBoundary`'s legacy end-of-parent fallback.
|
|
262
|
+
*/
|
|
263
|
+
function isTopLevelCommentScopeNode(node: Node): boolean {
|
|
264
|
+
const parent = node.parentNode
|
|
265
|
+
if (!parent) return false
|
|
266
|
+
let depth = 0
|
|
267
|
+
let sib: Node | null = parent.firstChild
|
|
268
|
+
while (sib) {
|
|
269
|
+
if (sib === node) return depth > 0
|
|
270
|
+
if (sib.nodeType === Node.COMMENT_NODE) {
|
|
271
|
+
const value = (sib as Comment).nodeValue ?? ''
|
|
272
|
+
if (value.startsWith(BF_SCOPE_COMMENT_PREFIX)) depth++
|
|
273
|
+
else if (value.startsWith(BF_SCOPE_COMMENT_END_PREFIX) && depth > 0) depth--
|
|
274
|
+
}
|
|
275
|
+
sib = sib.nextSibling
|
|
276
|
+
}
|
|
277
|
+
return false
|
|
226
278
|
}
|
|
227
279
|
|
|
228
280
|
/**
|
|
@@ -271,10 +323,16 @@ export function find(
|
|
|
271
323
|
if (ignoreScope) return candidate
|
|
272
324
|
if (commentInfo) {
|
|
273
325
|
// Comment scope: top-level siblings in the comment range are always
|
|
274
|
-
// accepted (even if they have bf-s, like proxy elements).
|
|
275
|
-
//
|
|
326
|
+
// accepted (even if they have bf-s, like proxy elements). A descendant
|
|
327
|
+
// nested inside one of those siblings is accepted unless a *nested*
|
|
328
|
+
// child scope sits between it and this one — i.e. its nearest bf-s
|
|
329
|
+
// ancestor falls within this comment's own sibling range. An ancestor
|
|
330
|
+
// bf-s element outside that range doesn't disqualify the candidate:
|
|
331
|
+
// a fragment-root child mounted inside a normal parent island always
|
|
332
|
+
// has one, and `.closest()` alone can't tell the two apart (#2302).
|
|
276
333
|
if (candidate.parentElement === commentInfo.commentNode.parentElement) return candidate
|
|
277
|
-
|
|
334
|
+
const nearestScope = candidate.closest(`[${BF_SCOPE}]`)
|
|
335
|
+
if (!nearestScope || !isInCommentScopeRange(nearestScope, commentInfo.commentNode)) return candidate
|
|
278
336
|
} else {
|
|
279
337
|
if (belongsToScope(candidate, scope)) return candidate
|
|
280
338
|
}
|
|
@@ -287,6 +345,44 @@ export function find(
|
|
|
287
345
|
return null
|
|
288
346
|
}
|
|
289
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Find a conditional's own target (a `bf-c="id"` element, for `insert.ts`)
|
|
350
|
+
* within `scope`'s content range.
|
|
351
|
+
*
|
|
352
|
+
* Deliberately narrower than `find()`: for a regular (non-comment) scope,
|
|
353
|
+
* this is a **plain, unfiltered** `scope.querySelector(selector)` — not
|
|
354
|
+
* `find()`'s `belongsToScope`-gated search. `belongsToScope` accepts a
|
|
355
|
+
* candidate only if `candidate.closest('[bf-s]')` is *exactly* `scope` —
|
|
356
|
+
* which can never hold when `scope` itself carries no `bf-s`. That's the
|
|
357
|
+
* common case for `insert()`'s `region.bindScope`: a `mapArray` loop item's
|
|
358
|
+
* cloned template root (e.g. a `.comment-item` `<div>`) is keyed by
|
|
359
|
+
* `data-key` for reconciliation, not given its own `bf-s`, so any `bf-c`
|
|
360
|
+
* conditional inside that item would be rejected outright by
|
|
361
|
+
* `belongsToScope` — not because of any nested-child-scope subtlety, just
|
|
362
|
+
* because `scope` itself isn't `bf-s`-addressable. Using `find()` here
|
|
363
|
+
* previously broke exactly that shape (piconic-ai/barefootjs#2313's first
|
|
364
|
+
* attempt, caught by the `site/ui` e2e suite's `SocialThreadDemo`
|
|
365
|
+
* comment-editing test — a per-comment `editing` conditional inside a
|
|
366
|
+
* `sortedComments().map(...)` loop item with no `bf-s` of its own).
|
|
367
|
+
*
|
|
368
|
+
* A comment-scope proxy (fragment-root component) still gets the
|
|
369
|
+
* comment-range-bounded, nested-fragment-excluding search — the same rule
|
|
370
|
+
* `find()`'s comment branch uses — since that IS what's needed to walk past
|
|
371
|
+
* a fragment-root's own top-level siblings correctly (#2312).
|
|
372
|
+
*/
|
|
373
|
+
export function findCondTarget(scope: Element, selector: string): Element | null {
|
|
374
|
+
const commentInfo = commentScopeRegistry.get(scope)
|
|
375
|
+
if (!commentInfo) {
|
|
376
|
+
return scope.querySelector(selector)
|
|
377
|
+
}
|
|
378
|
+
for (const candidate of candidatesInScope(scope, selector)) {
|
|
379
|
+
if (candidate.parentElement === commentInfo.commentNode.parentElement) return candidate
|
|
380
|
+
const nearestScope = candidate.closest(`[${BF_SCOPE}]`)
|
|
381
|
+
if (!nearestScope || !isInCommentScopeRange(nearestScope, commentInfo.commentNode)) return candidate
|
|
382
|
+
}
|
|
383
|
+
return null
|
|
384
|
+
}
|
|
385
|
+
|
|
290
386
|
/**
|
|
291
387
|
* Search in portals owned by a scope.
|
|
292
388
|
*/
|
|
@@ -551,8 +647,17 @@ export function findCommentChildScope(
|
|
|
551
647
|
* sibling range for comment-anchored scopes, the element subtree
|
|
552
648
|
* otherwise. Mirrors candidatesInScope's notion of "where the scope's
|
|
553
649
|
* content lives", but yields comments instead of elements.
|
|
650
|
+
*
|
|
651
|
+
* Exported for `insert.ts`'s branch-swap path (`updateFragmentConditional`),
|
|
652
|
+
* which otherwise walked `scope`'s own descendants with a bare
|
|
653
|
+
* `document.createTreeWalker` — never finding a conditional's
|
|
654
|
+
* `bf-cond-start:`/`bf-cond-end:` markers when they sit as a *sibling* of
|
|
655
|
+
* the scope's comment-scope proxy rather than nested inside it (true for
|
|
656
|
+
* any fragment-root component's own top-level conditional, since the proxy
|
|
657
|
+
* is one specific top-level element and the conditional's markers may be
|
|
658
|
+
* others). See piconic-ai/sora's `ListSidebar` for the real-world repro.
|
|
554
659
|
*/
|
|
555
|
-
function* commentsInScope(scope: Element): Generator<Comment> {
|
|
660
|
+
export function* commentsInScope(scope: Element): Generator<Comment> {
|
|
556
661
|
const commentInfo = commentScopeRegistry.get(scope)
|
|
557
662
|
|
|
558
663
|
if (commentInfo) {
|