@barefootjs/client 0.5.0 → 0.5.2
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/component.d.ts +1 -1
- package/dist/runtime/component.d.ts.map +1 -1
- package/dist/runtime/dynamic-text.d.ts +20 -0
- package/dist/runtime/dynamic-text.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +292 -26
- package/dist/runtime/insert.d.ts +1 -1
- package/dist/runtime/insert.d.ts.map +1 -1
- package/dist/runtime/map-array.d.ts +9 -0
- package/dist/runtime/map-array.d.ts.map +1 -1
- package/dist/runtime/scope.d.ts +7 -1
- package/dist/runtime/scope.d.ts.map +1 -1
- package/dist/runtime/standalone.js +292 -26
- package/package.json +2 -2
- package/src/runtime/component.ts +6 -1
- package/src/runtime/dynamic-text.ts +88 -0
- package/src/runtime/index.ts +2 -1
- package/src/runtime/insert.ts +124 -26
- package/src/runtime/map-array.ts +227 -0
- package/src/runtime/scope.ts +18 -5
|
@@ -304,6 +304,9 @@ var BF_SCOPE_COMMENT_PREFIX = "bf-scope:";
|
|
|
304
304
|
var BF_LOOP_START = "bf-loop";
|
|
305
305
|
var BF_LOOP_END = "bf-/loop";
|
|
306
306
|
var BF_LOOP_ITEM = "bf-loop-i";
|
|
307
|
+
function loopItemMarker(key) {
|
|
308
|
+
return `${BF_LOOP_ITEM}:${key}`;
|
|
309
|
+
}
|
|
307
310
|
function loopStartMarker(markerId) {
|
|
308
311
|
return `${BF_LOOP_START}:${markerId}`;
|
|
309
312
|
}
|
|
@@ -552,10 +555,18 @@ function getPortalScopeId(element) {
|
|
|
552
555
|
return info?.scopeId ?? null;
|
|
553
556
|
}
|
|
554
557
|
function getCommentScopeBoundary(commentNode) {
|
|
558
|
+
const isLoopItem = commentNode.nodeValue?.startsWith(`${BF_LOOP_ITEM}:`) ?? false;
|
|
555
559
|
let node = commentNode.nextSibling;
|
|
556
560
|
while (node) {
|
|
557
|
-
if (node.nodeType === Node.COMMENT_NODE
|
|
558
|
-
|
|
561
|
+
if (node.nodeType === Node.COMMENT_NODE) {
|
|
562
|
+
const value = node.nodeValue ?? "";
|
|
563
|
+
if (isLoopItem) {
|
|
564
|
+
if (value.startsWith(`${BF_LOOP_ITEM}:`) || value.startsWith(`${BF_LOOP_END}:`)) {
|
|
565
|
+
return node;
|
|
566
|
+
}
|
|
567
|
+
} else if (value.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
|
|
568
|
+
return node;
|
|
569
|
+
}
|
|
559
570
|
}
|
|
560
571
|
node = node.nextSibling;
|
|
561
572
|
}
|
|
@@ -1131,7 +1142,9 @@ function setParentScopeId(id) {
|
|
|
1131
1142
|
}
|
|
1132
1143
|
var propsUpdateMap = new WeakMap;
|
|
1133
1144
|
var propsMap = new WeakMap;
|
|
1134
|
-
function createComponent(nameOrDef, props, key, slot) {
|
|
1145
|
+
function createComponent(nameOrDef, props = {}, key, slot) {
|
|
1146
|
+
if (props == null)
|
|
1147
|
+
props = {};
|
|
1135
1148
|
if (typeof nameOrDef !== "string") {
|
|
1136
1149
|
return createComponentFromDef(nameOrDef, props, key);
|
|
1137
1150
|
}
|
|
@@ -1915,6 +1928,150 @@ function mapArray(accessor, container, getKey, renderItem, markerId) {
|
|
|
1915
1928
|
}
|
|
1916
1929
|
});
|
|
1917
1930
|
}
|
|
1931
|
+
var ITEM_PREFIX = `${BF_LOOP_ITEM}:`;
|
|
1932
|
+
function isItemAnchor(node) {
|
|
1933
|
+
return node.nodeType === Node.COMMENT_NODE && (node.nodeValue ?? "").startsWith(ITEM_PREFIX);
|
|
1934
|
+
}
|
|
1935
|
+
function collectAnchorRange(anchor, end) {
|
|
1936
|
+
const nodes = [anchor];
|
|
1937
|
+
let node = anchor.nextSibling;
|
|
1938
|
+
while (node && node !== end) {
|
|
1939
|
+
if (isItemAnchor(node))
|
|
1940
|
+
break;
|
|
1941
|
+
nodes.push(node);
|
|
1942
|
+
node = node.nextSibling;
|
|
1943
|
+
}
|
|
1944
|
+
return nodes;
|
|
1945
|
+
}
|
|
1946
|
+
function findItemAnchors(start, end) {
|
|
1947
|
+
const anchors = [];
|
|
1948
|
+
let node = start.nextSibling;
|
|
1949
|
+
while (node && node !== end) {
|
|
1950
|
+
if (isItemAnchor(node))
|
|
1951
|
+
anchors.push(node);
|
|
1952
|
+
node = node.nextSibling;
|
|
1953
|
+
}
|
|
1954
|
+
return anchors;
|
|
1955
|
+
}
|
|
1956
|
+
function placeAnchorScope(scope, container, before, end) {
|
|
1957
|
+
if (scope.pending) {
|
|
1958
|
+
container.insertBefore(scope.pending, before);
|
|
1959
|
+
scope.pending = null;
|
|
1960
|
+
return;
|
|
1961
|
+
}
|
|
1962
|
+
for (const node of collectAnchorRange(scope.anchor, end)) {
|
|
1963
|
+
container.insertBefore(node, before);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
function removeAnchorScope(scope, end) {
|
|
1967
|
+
for (const node of collectAnchorRange(scope.anchor, end)) {
|
|
1968
|
+
node.parentNode?.removeChild(node);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
function createAnchorScope(item, index, key, renderItem, existingAnchor) {
|
|
1972
|
+
let dispose;
|
|
1973
|
+
let setItem;
|
|
1974
|
+
let returned;
|
|
1975
|
+
createRoot((d) => {
|
|
1976
|
+
dispose = d;
|
|
1977
|
+
const [itemAccessor, itemSetter] = createSignal(item);
|
|
1978
|
+
setItem = itemSetter;
|
|
1979
|
+
returned = renderItem(itemAccessor, index, existingAnchor);
|
|
1980
|
+
return;
|
|
1981
|
+
});
|
|
1982
|
+
if (existingAnchor) {
|
|
1983
|
+
return { anchor: existingAnchor, pending: null, dispose, setItem };
|
|
1984
|
+
}
|
|
1985
|
+
const frag = returned;
|
|
1986
|
+
const anchor = frag.firstChild;
|
|
1987
|
+
if (anchor && !anchor.nodeValue?.startsWith(ITEM_PREFIX)) {
|
|
1988
|
+
anchor.nodeValue = loopItemMarker(key);
|
|
1989
|
+
}
|
|
1990
|
+
return { anchor, pending: frag, dispose, setItem };
|
|
1991
|
+
}
|
|
1992
|
+
function mapArrayAnchored(accessor, container, getKey, renderItem, markerId) {
|
|
1993
|
+
if (!container)
|
|
1994
|
+
return;
|
|
1995
|
+
const scopes = new Map;
|
|
1996
|
+
let hydrated = false;
|
|
1997
|
+
createEffect(() => {
|
|
1998
|
+
const items = accessor();
|
|
1999
|
+
if (!items)
|
|
2000
|
+
return;
|
|
2001
|
+
const { start, end } = findLoopMarkers2(container, markerId);
|
|
2002
|
+
if (!start || !end)
|
|
2003
|
+
return;
|
|
2004
|
+
if (!hydrated) {
|
|
2005
|
+
hydrated = true;
|
|
2006
|
+
const existing = findItemAnchors(start, end);
|
|
2007
|
+
if (existing.length > 0 && scopes.size === 0) {
|
|
2008
|
+
for (let i = 0;i < existing.length && i < items.length; i++) {
|
|
2009
|
+
const item = items[i];
|
|
2010
|
+
const key = getKey ? getKey(item, i) : String(i);
|
|
2011
|
+
scopes.set(key, createAnchorScope(item, i, key, renderItem, existing[i]));
|
|
2012
|
+
}
|
|
2013
|
+
for (let i = existing.length;i < items.length; i++) {
|
|
2014
|
+
const item = items[i];
|
|
2015
|
+
const key = getKey ? getKey(item, i) : String(i);
|
|
2016
|
+
const scope = createAnchorScope(item, i, key, renderItem);
|
|
2017
|
+
scopes.set(key, scope);
|
|
2018
|
+
placeAnchorScope(scope, container, end, end);
|
|
2019
|
+
}
|
|
2020
|
+
for (let i = items.length;i < existing.length; i++) {
|
|
2021
|
+
for (const node of collectAnchorRange(existing[i], end)) {
|
|
2022
|
+
node.parentNode?.removeChild(node);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
const newKeys = new Set;
|
|
2029
|
+
const warnedKeys = new Set;
|
|
2030
|
+
const desiredOrder = [];
|
|
2031
|
+
for (let i = 0;i < items.length; i++) {
|
|
2032
|
+
const item = items[i];
|
|
2033
|
+
const key = getKey ? getKey(item, i) : String(i);
|
|
2034
|
+
if (newKeys.has(key) && !warnedKeys.has(key)) {
|
|
2035
|
+
warnedKeys.add(key);
|
|
2036
|
+
console.warn(`[BarefootJS] mapArrayAnchored: duplicate key "${key}" — items with this key collapse to a ` + `single DOM scope, so only the last one renders. Use a per-item identifier (e.g. \`key={item.id}\`).`);
|
|
2037
|
+
}
|
|
2038
|
+
newKeys.add(key);
|
|
2039
|
+
const existing = scopes.get(key);
|
|
2040
|
+
if (existing) {
|
|
2041
|
+
existing.setItem(item);
|
|
2042
|
+
desiredOrder.push(existing);
|
|
2043
|
+
} else {
|
|
2044
|
+
const scope = createAnchorScope(item, i, key, renderItem);
|
|
2045
|
+
scopes.set(key, scope);
|
|
2046
|
+
desiredOrder.push(scope);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
for (const [key, scope] of scopes) {
|
|
2050
|
+
if (!newKeys.has(key)) {
|
|
2051
|
+
scope.dispose();
|
|
2052
|
+
removeAnchorScope(scope, end);
|
|
2053
|
+
scopes.delete(key);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
let inOrder = true;
|
|
2057
|
+
const domAnchors = findItemAnchors(start, end);
|
|
2058
|
+
if (domAnchors.length !== desiredOrder.length) {
|
|
2059
|
+
inOrder = false;
|
|
2060
|
+
} else {
|
|
2061
|
+
for (let i = 0;i < desiredOrder.length; i++) {
|
|
2062
|
+
if (domAnchors[i] !== desiredOrder[i].anchor) {
|
|
2063
|
+
inOrder = false;
|
|
2064
|
+
break;
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
if (!inOrder) {
|
|
2069
|
+
for (const scope of desiredOrder) {
|
|
2070
|
+
placeAnchorScope(scope, container, end, end);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
1918
2075
|
// src/runtime/style.ts
|
|
1919
2076
|
function styleToCss(value) {
|
|
1920
2077
|
if (value == null)
|
|
@@ -2019,6 +2176,55 @@ function spreadAttrs(obj) {
|
|
|
2019
2176
|
return parts.join(" ");
|
|
2020
2177
|
}
|
|
2021
2178
|
// src/runtime/insert.ts
|
|
2179
|
+
function makeRegion(scope) {
|
|
2180
|
+
if (scope.nodeType === Node.COMMENT_NODE) {
|
|
2181
|
+
const anchor = scope;
|
|
2182
|
+
const parentEl = anchor.parentElement;
|
|
2183
|
+
const componentScope = parentEl?.closest(`[${BF_SCOPE}]`) ?? null;
|
|
2184
|
+
const parentScopeId = componentScope?.getAttribute(BF_SCOPE) ?? null;
|
|
2185
|
+
const proxyEl = document.createElement("bf-loop-item");
|
|
2186
|
+
commentScopeRegistry.set(proxyEl, { commentNode: anchor, scopeId: parentScopeId ?? "" });
|
|
2187
|
+
return { anchor, bindScope: proxyEl, parentScopeId };
|
|
2188
|
+
}
|
|
2189
|
+
const el = scope;
|
|
2190
|
+
return { anchor: null, bindScope: el, parentScopeId: el.getAttribute(BF_SCOPE) };
|
|
2191
|
+
}
|
|
2192
|
+
function findCondElInRange(anchor, id) {
|
|
2193
|
+
const sel = `[${BF_COND}="${id}"]`;
|
|
2194
|
+
const boundary = getCommentScopeBoundary(anchor);
|
|
2195
|
+
let node = anchor.nextSibling;
|
|
2196
|
+
while (node && node !== boundary) {
|
|
2197
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
2198
|
+
const el = node;
|
|
2199
|
+
if (el.matches?.(sel))
|
|
2200
|
+
return el;
|
|
2201
|
+
const inner = el.querySelector(sel);
|
|
2202
|
+
if (inner)
|
|
2203
|
+
return inner;
|
|
2204
|
+
}
|
|
2205
|
+
node = node.nextSibling;
|
|
2206
|
+
}
|
|
2207
|
+
return null;
|
|
2208
|
+
}
|
|
2209
|
+
function findCondStartInRange(anchor, id) {
|
|
2210
|
+
const want = `bf-cond-start:${id}`;
|
|
2211
|
+
const boundary = getCommentScopeBoundary(anchor);
|
|
2212
|
+
let node = anchor.nextSibling;
|
|
2213
|
+
while (node && node !== boundary) {
|
|
2214
|
+
if (node.nodeType === Node.COMMENT_NODE && node.nodeValue === want) {
|
|
2215
|
+
return node;
|
|
2216
|
+
}
|
|
2217
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
2218
|
+
const w = document.createTreeWalker(node, NodeFilter.SHOW_COMMENT);
|
|
2219
|
+
while (w.nextNode()) {
|
|
2220
|
+
if (w.currentNode.nodeValue === want)
|
|
2221
|
+
return w.currentNode;
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
node = node.nextSibling;
|
|
2225
|
+
}
|
|
2226
|
+
return null;
|
|
2227
|
+
}
|
|
2022
2228
|
var EMPTY_SLOTS = [];
|
|
2023
2229
|
function normalizeTemplate(value) {
|
|
2024
2230
|
return typeof value === "string" ? { html: value, slots: EMPTY_SLOTS } : value;
|
|
@@ -2029,7 +2235,8 @@ function evalBranchTemplate(branch) {
|
|
|
2029
2235
|
function insert(scope, id, conditionFn, whenTrue, whenFalse) {
|
|
2030
2236
|
if (!scope)
|
|
2031
2237
|
return;
|
|
2032
|
-
const
|
|
2238
|
+
const region = makeRegion(scope);
|
|
2239
|
+
const parentScopeId = region.parentScopeId;
|
|
2033
2240
|
let isFragmentCond = false;
|
|
2034
2241
|
try {
|
|
2035
2242
|
const sampleTrue = evalBranchTemplate(whenTrue);
|
|
@@ -2072,23 +2279,23 @@ function insert(scope, id, conditionFn, whenTrue, whenFalse) {
|
|
|
2072
2279
|
} finally {
|
|
2073
2280
|
setParentScopeId(null);
|
|
2074
2281
|
}
|
|
2075
|
-
const existingEl = find(
|
|
2282
|
+
const existingEl = region.anchor ? findCondElInRange(region.anchor, id) : find(region.bindScope, `[${BF_COND}="${id}"]`);
|
|
2076
2283
|
if (existingEl) {
|
|
2077
2284
|
const expectedSig = getTemplateRootSignature(result2.html);
|
|
2078
2285
|
const existingSig = existingEl.outerHTML.match(/^<[^>]+>/)?.[0] ?? null;
|
|
2079
2286
|
if (isFragmentCond && (!expectedSig || existingSig !== expectedSig)) {
|
|
2080
|
-
updateFragmentConditional(
|
|
2287
|
+
updateFragmentConditional(region, id, result2);
|
|
2081
2288
|
} else if (!isFragmentCond && expectedSig && existingSig && expectedSig !== existingSig) {
|
|
2082
|
-
updateElementConditional(
|
|
2289
|
+
updateElementConditional(region, id, result2);
|
|
2083
2290
|
} else if (result2.slots.length > 0) {
|
|
2084
|
-
updateElementConditional(
|
|
2291
|
+
updateElementConditional(region, id, result2);
|
|
2085
2292
|
}
|
|
2086
2293
|
} else if (isFragmentCond) {
|
|
2087
|
-
updateFragmentConditional(
|
|
2294
|
+
updateFragmentConditional(region, id, result2);
|
|
2088
2295
|
}
|
|
2089
|
-
const cleanup2 = branch.bindEvents(
|
|
2296
|
+
const cleanup2 = branch.bindEvents(region.bindScope, { isFirstRun: true });
|
|
2090
2297
|
branchCleanup = typeof cleanup2 === "function" ? cleanup2 : null;
|
|
2091
|
-
autoFocusConditionalElement(
|
|
2298
|
+
autoFocusConditionalElement(region, id);
|
|
2092
2299
|
return;
|
|
2093
2300
|
}
|
|
2094
2301
|
if (currCond === prevVal) {
|
|
@@ -2106,18 +2313,18 @@ function insert(scope, id, conditionFn, whenTrue, whenFalse) {
|
|
|
2106
2313
|
setParentScopeId(null);
|
|
2107
2314
|
}
|
|
2108
2315
|
if (isFragmentCond) {
|
|
2109
|
-
updateFragmentConditional(
|
|
2316
|
+
updateFragmentConditional(region, id, result);
|
|
2110
2317
|
} else {
|
|
2111
|
-
updateElementConditional(
|
|
2318
|
+
updateElementConditional(region, id, result);
|
|
2112
2319
|
}
|
|
2113
|
-
const cleanup = branch.bindEvents(
|
|
2320
|
+
const cleanup = branch.bindEvents(region.bindScope, { isFirstRun: false });
|
|
2114
2321
|
branchCleanup = typeof cleanup === "function" ? cleanup : null;
|
|
2115
|
-
autoFocusConditionalElement(
|
|
2322
|
+
autoFocusConditionalElement(region, id);
|
|
2116
2323
|
});
|
|
2117
2324
|
}
|
|
2118
|
-
function autoFocusConditionalElement(
|
|
2325
|
+
function autoFocusConditionalElement(region, id) {
|
|
2119
2326
|
requestAnimationFrame(() => {
|
|
2120
|
-
const condEl =
|
|
2327
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : region.bindScope.querySelector(`[${BF_COND}="${id}"]`);
|
|
2121
2328
|
if (condEl) {
|
|
2122
2329
|
const autofocusEl = condEl.matches("[autofocus]") ? condEl : condEl.querySelector("[autofocus]");
|
|
2123
2330
|
if (autofocusEl && typeof autofocusEl.focus === "function") {
|
|
@@ -2150,18 +2357,23 @@ function spliceSlots(fragment, slots) {
|
|
|
2150
2357
|
}
|
|
2151
2358
|
return fragment;
|
|
2152
2359
|
}
|
|
2153
|
-
function updateFragmentConditional(
|
|
2360
|
+
function updateFragmentConditional(region, id, result) {
|
|
2154
2361
|
const { html, slots } = result;
|
|
2362
|
+
const scope = region.bindScope;
|
|
2155
2363
|
const startMarker = `bf-cond-start:${id}`;
|
|
2156
2364
|
let startComment = null;
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2365
|
+
if (region.anchor) {
|
|
2366
|
+
startComment = findCondStartInRange(region.anchor, id);
|
|
2367
|
+
} else {
|
|
2368
|
+
const walker = document.createTreeWalker(scope, NodeFilter.SHOW_COMMENT);
|
|
2369
|
+
while (walker.nextNode()) {
|
|
2370
|
+
if (walker.currentNode.nodeValue === startMarker) {
|
|
2371
|
+
startComment = walker.currentNode;
|
|
2372
|
+
break;
|
|
2373
|
+
}
|
|
2162
2374
|
}
|
|
2163
2375
|
}
|
|
2164
|
-
const condEl = scope.querySelector(`[${BF_COND}="${id}"]`);
|
|
2376
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : scope.querySelector(`[${BF_COND}="${id}"]`);
|
|
2165
2377
|
const endMarker = `bf-cond-end:${id}`;
|
|
2166
2378
|
if (startComment) {
|
|
2167
2379
|
const nodesToRemove = [];
|
|
@@ -2200,8 +2412,8 @@ function updateFragmentConditional(scope, id, result) {
|
|
|
2200
2412
|
}
|
|
2201
2413
|
}
|
|
2202
2414
|
}
|
|
2203
|
-
function updateElementConditional(
|
|
2204
|
-
const condEl =
|
|
2415
|
+
function updateElementConditional(region, id, result) {
|
|
2416
|
+
const condEl = region.anchor ? findCondElInRange(region.anchor, id) : region.bindScope.querySelector(`[${BF_COND}="${id}"]`);
|
|
2205
2417
|
if (!condEl)
|
|
2206
2418
|
return;
|
|
2207
2419
|
const { html, slots } = result;
|
|
@@ -2226,6 +2438,58 @@ function __bfSlot(value, slots) {
|
|
|
2226
2438
|
}
|
|
2227
2439
|
return String(value);
|
|
2228
2440
|
}
|
|
2441
|
+
// src/runtime/dynamic-text.ts
|
|
2442
|
+
var END_MARKER = "/";
|
|
2443
|
+
function clearSlotRegion(start, keep) {
|
|
2444
|
+
let n = start.nextSibling;
|
|
2445
|
+
while (n && !(n.nodeType === Node.COMMENT_NODE && n.nodeValue === END_MARKER)) {
|
|
2446
|
+
const next = n.nextSibling;
|
|
2447
|
+
if (n !== keep)
|
|
2448
|
+
n.parentNode?.removeChild(n);
|
|
2449
|
+
n = next;
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
function slotStart(node) {
|
|
2453
|
+
let n = node.previousSibling;
|
|
2454
|
+
while (n && n.nodeType !== Node.COMMENT_NODE)
|
|
2455
|
+
n = n.previousSibling;
|
|
2456
|
+
return n;
|
|
2457
|
+
}
|
|
2458
|
+
function __bfText(current, value) {
|
|
2459
|
+
if (!current)
|
|
2460
|
+
return current;
|
|
2461
|
+
if (value != null && value.__isSlot)
|
|
2462
|
+
return current;
|
|
2463
|
+
if (typeof Node !== "undefined" && value instanceof Node) {
|
|
2464
|
+
if (value === current)
|
|
2465
|
+
return current;
|
|
2466
|
+
const start2 = current.previousSibling;
|
|
2467
|
+
if (start2 && start2.nodeType === Node.COMMENT_NODE) {
|
|
2468
|
+
clearSlotRegion(start2);
|
|
2469
|
+
start2.parentNode?.insertBefore(value, start2.nextSibling);
|
|
2470
|
+
return value;
|
|
2471
|
+
}
|
|
2472
|
+
current.parentNode?.replaceChild(value, current);
|
|
2473
|
+
return value;
|
|
2474
|
+
}
|
|
2475
|
+
const text = String(value ?? "");
|
|
2476
|
+
if (current.nodeType === Node.TEXT_NODE) {
|
|
2477
|
+
current.nodeValue = text;
|
|
2478
|
+
const start2 = slotStart(current);
|
|
2479
|
+
if (start2 && start2.nodeType === Node.COMMENT_NODE)
|
|
2480
|
+
clearSlotRegion(start2, current);
|
|
2481
|
+
return current;
|
|
2482
|
+
}
|
|
2483
|
+
const start = current.previousSibling;
|
|
2484
|
+
const textNode = (current.ownerDocument ?? document).createTextNode(text);
|
|
2485
|
+
if (start && start.nodeType === Node.COMMENT_NODE) {
|
|
2486
|
+
clearSlotRegion(start);
|
|
2487
|
+
start.parentNode?.insertBefore(textNode, start.nextSibling);
|
|
2488
|
+
} else {
|
|
2489
|
+
current.parentNode?.replaceChild(textNode, current);
|
|
2490
|
+
}
|
|
2491
|
+
return textNode;
|
|
2492
|
+
}
|
|
2229
2493
|
// src/runtime/client-marker.ts
|
|
2230
2494
|
function updateClientMarker(scope, id, value) {
|
|
2231
2495
|
if (!scope)
|
|
@@ -2346,6 +2610,7 @@ export {
|
|
|
2346
2610
|
parseHTML,
|
|
2347
2611
|
onMount,
|
|
2348
2612
|
onCleanup,
|
|
2613
|
+
mapArrayAnchored,
|
|
2349
2614
|
mapArray,
|
|
2350
2615
|
isSSRPortal,
|
|
2351
2616
|
insert,
|
|
@@ -2379,6 +2644,7 @@ export {
|
|
|
2379
2644
|
applyRestAttrs,
|
|
2380
2645
|
__slot,
|
|
2381
2646
|
__bf_swap,
|
|
2647
|
+
__bfText,
|
|
2382
2648
|
__bfSlot,
|
|
2383
2649
|
$t,
|
|
2384
2650
|
$c,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/client",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
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.5.
|
|
58
|
+
"@barefootjs/shared": "0.5.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@barefootjs/jsx": ">=0.2.0"
|
package/src/runtime/component.ts
CHANGED
|
@@ -74,10 +74,15 @@ export interface CreateComponentSlotInfo {
|
|
|
74
74
|
|
|
75
75
|
export function createComponent(
|
|
76
76
|
nameOrDef: string | ComponentDef,
|
|
77
|
-
props: Record<string, unknown
|
|
77
|
+
props: Record<string, unknown> = {},
|
|
78
78
|
key?: string | number,
|
|
79
79
|
slot?: CreateComponentSlotInfo,
|
|
80
80
|
): HTMLElement {
|
|
81
|
+
// A bare callable shim invoked from user code (e.g. an object-literal
|
|
82
|
+
// value `LOGOS[id]()` whose arrow the compiler hoisted into a component)
|
|
83
|
+
// reaches us with no props (#1663). Normalize to an empty object so the
|
|
84
|
+
// descriptor probes below don't throw on `undefined`.
|
|
85
|
+
if (props == null) props = {}
|
|
81
86
|
// ComponentDef mode: use def directly instead of registry lookup
|
|
82
87
|
if (typeof nameOrDef !== 'string') {
|
|
83
88
|
return createComponentFromDef(nameOrDef, props, key)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic text/JSX slot updater (#1663).
|
|
3
|
+
*
|
|
4
|
+
* The compiler wraps reactive child expressions (`<div>{expr}</div>`) in a
|
|
5
|
+
* `createEffect` that writes the value into the text node sitting between
|
|
6
|
+
* the slot's `<!--bf:sX-->` / `<!--/-->` comment markers. That was a pure
|
|
7
|
+
* `nodeValue = String(value)` assignment, which is correct for primitives
|
|
8
|
+
* but destroys a live `Node` — e.g. when `expr` is a JSX-returning call such
|
|
9
|
+
* as `{themeLogo(id)}` / `{LOGOS[id]()}` whose value is the `HTMLElement`
|
|
10
|
+
* returned by `createComponent`. Stringifying it produced
|
|
11
|
+
* `"[object HTMLElement]"` (and clobbered the server-rendered subtree).
|
|
12
|
+
*
|
|
13
|
+
* `__bfText` mirrors `__bfSlot` (the branch-template equivalent): when the
|
|
14
|
+
* value is a `Node`, it replaces the slot region with that node by identity;
|
|
15
|
+
* otherwise it behaves exactly like the previous text assignment. It returns
|
|
16
|
+
* the node that now occupies the slot so the caller can track it across
|
|
17
|
+
* reactive re-runs (the previous node is detached once replaced).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const END_MARKER = '/'
|
|
21
|
+
|
|
22
|
+
/** Remove every sibling between `start` (the `<!--bf:sX-->` comment) and the
|
|
23
|
+
* matching `<!--/-->` end comment, leaving both markers in place. When `keep`
|
|
24
|
+
* is supplied that node is left in place (used when writing a primitive
|
|
25
|
+
* through a text anchor that must survive while stale siblings are cleared). */
|
|
26
|
+
function clearSlotRegion(start: Node, keep?: Node): void {
|
|
27
|
+
let n = start.nextSibling
|
|
28
|
+
while (
|
|
29
|
+
n &&
|
|
30
|
+
!(n.nodeType === Node.COMMENT_NODE && (n as Comment).nodeValue === END_MARKER)
|
|
31
|
+
) {
|
|
32
|
+
const next = n.nextSibling
|
|
33
|
+
if (n !== keep) n.parentNode?.removeChild(n)
|
|
34
|
+
n = next
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Walk back from `node` to the nearest preceding comment marker (the slot's
|
|
39
|
+
* `<!--bf:sX-->` start), skipping any stale element siblings in between. */
|
|
40
|
+
function slotStart(node: Node): Node | null {
|
|
41
|
+
let n = node.previousSibling
|
|
42
|
+
while (n && n.nodeType !== Node.COMMENT_NODE) n = n.previousSibling
|
|
43
|
+
return n
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function __bfText(current: Node | null, value: unknown): Node | null {
|
|
47
|
+
if (!current) return current
|
|
48
|
+
// Slot markers (`__slot()`): leave the server-rendered DOM untouched.
|
|
49
|
+
if (value != null && (value as { __isSlot?: boolean }).__isSlot) return current
|
|
50
|
+
|
|
51
|
+
if (typeof Node !== 'undefined' && value instanceof Node) {
|
|
52
|
+
if (value === current) return current
|
|
53
|
+
const start = current.previousSibling
|
|
54
|
+
if (start && start.nodeType === Node.COMMENT_NODE) {
|
|
55
|
+
clearSlotRegion(start)
|
|
56
|
+
start.parentNode?.insertBefore(value, start.nextSibling)
|
|
57
|
+
return value
|
|
58
|
+
}
|
|
59
|
+
// No marker to anchor against — best-effort in-place replacement.
|
|
60
|
+
current.parentNode?.replaceChild(value, current)
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const text = String(value ?? '')
|
|
65
|
+
if (current.nodeType === Node.TEXT_NODE) {
|
|
66
|
+
current.nodeValue = text
|
|
67
|
+
// The conditional-slot path re-resolves the anchor via `$t()` on every
|
|
68
|
+
// run, which can hand back a freshly created text node sitting *before* a
|
|
69
|
+
// stale element left by a previous Node-valued run. Clear any remaining
|
|
70
|
+
// siblings up to the end marker so switching JSX → text doesn't render
|
|
71
|
+
// both the old element and the new text.
|
|
72
|
+
const start = slotStart(current)
|
|
73
|
+
if (start && start.nodeType === Node.COMMENT_NODE) clearSlotRegion(start, current)
|
|
74
|
+
return current
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Switching back from a Node value to text: drop the element and restore a
|
|
78
|
+
// text node in the slot region.
|
|
79
|
+
const start = current.previousSibling
|
|
80
|
+
const textNode = (current.ownerDocument ?? document).createTextNode(text)
|
|
81
|
+
if (start && start.nodeType === Node.COMMENT_NODE) {
|
|
82
|
+
clearSlotRegion(start)
|
|
83
|
+
start.parentNode?.insertBefore(textNode, start.nextSibling)
|
|
84
|
+
} else {
|
|
85
|
+
current.parentNode?.replaceChild(textNode, current)
|
|
86
|
+
}
|
|
87
|
+
return textNode
|
|
88
|
+
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -56,7 +56,7 @@ export {
|
|
|
56
56
|
export { reconcileList, type RenderItemFn } from './list'
|
|
57
57
|
export { reconcileElements, getLoopChildren, getLoopNodes } from './reconcile-elements'
|
|
58
58
|
export { qsaItem, upsertChildItem } from './qsa-item'
|
|
59
|
-
export { mapArray } from './map-array'
|
|
59
|
+
export { mapArray, mapArrayAnchored } from './map-array'
|
|
60
60
|
|
|
61
61
|
// Template registry
|
|
62
62
|
export { registerTemplate, getTemplate, hasTemplate, type TemplateFn } from './template'
|
|
@@ -81,6 +81,7 @@ export { hydrate, rehydrateAll, flushHydration, getRegisteredDef } from './hydra
|
|
|
81
81
|
export { registerComponent, getComponentInit, initChild, upsertChild } from './registry'
|
|
82
82
|
export { insert, type BranchConfig, type BranchTemplateResult } from './insert'
|
|
83
83
|
export { __bfSlot } from './branch-slot'
|
|
84
|
+
export { __bfText } from './dynamic-text'
|
|
84
85
|
export { updateClientMarker } from './client-marker'
|
|
85
86
|
|
|
86
87
|
// Hydration state
|