@stacksjs/stx 0.2.118 → 0.2.120
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/builtins/attrs.d.ts +16 -0
- package/dist/builtins/attrs.js +16 -0
- package/dist/builtins/safe-image.js +2 -9
- package/dist/builtins/stx-image.js +2 -9
- package/dist/builtins/stx-link.js +2 -9
- package/dist/cli.js +1869 -824
- package/dist/client-script-bundler.d.ts +0 -11
- package/dist/client-script-bundler.js +26 -4
- package/dist/client-script.d.ts +24 -0
- package/dist/client-script.js +59 -5
- package/dist/component-registry.d.ts +1 -0
- package/dist/component-renderer.js +13 -5
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +1 -0
- package/dist/composables/use-scroll-lock.d.ts +13 -0
- package/dist/composables/use-scroll-lock.js +55 -0
- package/dist/composables/use-timer.d.ts +1 -1
- package/dist/composables/use-timer.js +13 -3
- package/dist/composition-api.d.ts +1 -1
- package/dist/composition-api.js +13 -4
- package/dist/craft-bridge.d.ts +80 -1
- package/dist/craft-bridge.js +80 -1
- package/dist/craft-components.d.ts +77 -3
- package/dist/craft-components.js +138 -9
- package/dist/craft.js +481 -73
- package/dist/dev-server/crosswind.d.ts +15 -1
- package/dist/dev-server/crosswind.js +12 -3
- package/dist/dev-server/serve-app.js +3 -3
- package/dist/document-shell.d.ts +17 -0
- package/dist/document-shell.js +72 -2
- package/dist/expressions.js +14 -7
- package/dist/head.d.ts +8 -0
- package/dist/head.js +11 -8
- package/dist/html-masking.d.ts +12 -0
- package/dist/html-masking.js +50 -15
- package/dist/index.d.ts +9 -1
- package/dist/index.js +9 -1
- package/dist/process.js +11 -3
- package/dist/pwa.js +1172 -431
- package/dist/runtime-globals.d.ts +7 -4
- package/dist/runtime-globals.js +10 -2
- package/dist/runtime-injection.js +15 -1
- package/dist/signal-processing.js +19 -28
- package/dist/signals-api.d.ts +2 -0
- package/dist/signals-api.js +9 -0
- package/dist/signals.js +802 -62
- package/dist/ssg.js +1161 -420
- package/dist/state-management.d.ts +1 -1
- package/dist/state-management.js +1 -1
- package/dist/strict-lint.d.ts +1 -1
- package/dist/strict-lint.js +1 -1
- package/dist/stx.d.ts +6 -0
- package/dist/style-scoping.js +10 -0
- package/dist/utils.js +5 -3
- package/dist/variable-extractor.js +14 -22
- package/dist/vue-template.js +3 -2
- package/dist/x-element.d.ts +2 -2
- package/dist/x-element.js +9 -8
- package/package.json +5 -5
- package/stx.d.ts +6 -0
package/dist/signals.js
CHANGED
|
@@ -270,6 +270,12 @@ else {
|
|
|
270
270
|
const val = target[prop];
|
|
271
271
|
// If it's a signal or derived, call it to get the value
|
|
272
272
|
if (val && typeof val === 'function' && (val._isSignal || val._isDerived)) {
|
|
273
|
+
// Loop variables are implementation-detail signals so keyed rows can
|
|
274
|
+
// update reactively. In template expressions they must behave like
|
|
275
|
+
// their item value, including for objects with fields named value,
|
|
276
|
+
// set, or subscribe. Otherwise item.value is mistaken for the
|
|
277
|
+
// signal compatibility API and resolves to the whole item object.
|
|
278
|
+
if (val._isStxLoopItem) return val();
|
|
273
279
|
if (preserveSignal && preserveSignal(prop, val)) return val;
|
|
274
280
|
return val();
|
|
275
281
|
}
|
|
@@ -1274,7 +1280,7 @@ finally {
|
|
|
1274
1280
|
return null;
|
|
1275
1281
|
}
|
|
1276
1282
|
|
|
1277
|
-
function toValue(expr, el, enableAutoUnwrap = true) {
|
|
1283
|
+
function toValue(expr, el, enableAutoUnwrap = true, extraScope = {}) {
|
|
1278
1284
|
try {
|
|
1279
1285
|
// Skip placeholder expressions like __TITLE__ (build-time placeholders)
|
|
1280
1286
|
if (/^__[A-Z_]+__$/.test(expr.trim())) {
|
|
@@ -1282,7 +1288,7 @@ finally {
|
|
|
1282
1288
|
}
|
|
1283
1289
|
// First try component-level scope
|
|
1284
1290
|
const elementScope = findElementScope(el || currentElement);
|
|
1285
|
-
const baseScope = { ...globalHelpers, ...componentScope, ...(elementScope || {}) };
|
|
1291
|
+
const baseScope = { ...globalHelpers, ...componentScope, ...(elementScope || {}), ...extraScope };
|
|
1286
1292
|
|
|
1287
1293
|
// Check for pipe syntax (Feature #2)
|
|
1288
1294
|
const pipeResult = parsePipeExpression(expr, baseScope);
|
|
@@ -1361,8 +1367,8 @@ catch (e) {
|
|
|
1361
1367
|
const [, varName, op, valueExpr] = assignMatch;
|
|
1362
1368
|
const signal = scope[varName];
|
|
1363
1369
|
if (signal && signal._isSignal) {
|
|
1364
|
-
return () => {
|
|
1365
|
-
const addValue = toValue(valueExpr, null, true);
|
|
1370
|
+
return ($event) => {
|
|
1371
|
+
const addValue = toValue(valueExpr, null, true, { ...scope, $event: $event });
|
|
1366
1372
|
signal.update(n => {
|
|
1367
1373
|
switch (op) {
|
|
1368
1374
|
case '+': return n + addValue;
|
|
@@ -1383,8 +1389,8 @@ catch (e) {
|
|
|
1383
1389
|
const [, varName, valueExpr] = simpleAssignMatch;
|
|
1384
1390
|
const signal = scope[varName];
|
|
1385
1391
|
if (signal && signal._isSignal) {
|
|
1386
|
-
return () => {
|
|
1387
|
-
const newValue = toValue(valueExpr, null, true);
|
|
1392
|
+
return ($event) => {
|
|
1393
|
+
const newValue = toValue(valueExpr, null, true, { ...scope, $event: $event });
|
|
1388
1394
|
signal.set(newValue);
|
|
1389
1395
|
};
|
|
1390
1396
|
}
|
|
@@ -1467,8 +1473,16 @@ catch (e) {
|
|
|
1467
1473
|
return;
|
|
1468
1474
|
}
|
|
1469
1475
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1476
|
+
// Loop items arrive as signals (see createItemElements). Unwrap them for
|
|
1477
|
+
// the same reason the inline @event path does: \`item.value\` in a handler
|
|
1478
|
+
// means the item's field, not the signal API.
|
|
1479
|
+
const handlerScope = {};
|
|
1480
|
+
Object.keys(scope).forEach(function(k) {
|
|
1481
|
+
const v = scope[k];
|
|
1482
|
+
handlerScope[k] = (v && typeof v === 'function' && v._isStxLoopItem) ? v() : v;
|
|
1483
|
+
});
|
|
1484
|
+
const fn = new Function(...Object.keys(handlerScope), '$event', expr);
|
|
1485
|
+
fn(...Object.values(handlerScope), event);
|
|
1472
1486
|
}
|
|
1473
1487
|
catch (e) {
|
|
1474
1488
|
// Event handlers fire on user interaction, not reactive re-runs \u2014 there's no
|
|
@@ -1625,10 +1639,22 @@ catch (e) {
|
|
|
1625
1639
|
&& /^\\s*\\(?\\s*\\w+(?:\\s*,\\s*\\w+)?\\s*\\)?\\s+(?:in|of)\\s+.+\\s*$/.test(expr);
|
|
1626
1640
|
}
|
|
1627
1641
|
|
|
1642
|
+
function getElementAttributes(el) {
|
|
1643
|
+
if (!el || !el.attributes) return [];
|
|
1644
|
+
return Array.from(el.attributes).map(function(attr) {
|
|
1645
|
+
// Browser DOM exposes Attr records through NamedNodeMap. HappyDOM and
|
|
1646
|
+
// other standards-oriented DOM implementations may expose Map entries
|
|
1647
|
+
// for cloned nodes instead. Normalize both shapes so reactive nodes
|
|
1648
|
+
// created by :for hydrate exactly like their browser counterparts.
|
|
1649
|
+
if (Array.isArray(attr)) return { name: attr[0], value: attr[1] };
|
|
1650
|
+
return attr;
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1628
1654
|
function getModelBinding(el) {
|
|
1629
1655
|
if (!el || !el.attributes) return null;
|
|
1630
1656
|
var prefixes = ['x-model', ':model', '@model'];
|
|
1631
|
-
var attrs =
|
|
1657
|
+
var attrs = getElementAttributes(el);
|
|
1632
1658
|
for (var i = 0; i < attrs.length; i++) {
|
|
1633
1659
|
var name = attrs[i] && attrs[i].name;
|
|
1634
1660
|
if (!name) continue;
|
|
@@ -1640,17 +1666,112 @@ catch (e) {
|
|
|
1640
1666
|
return null;
|
|
1641
1667
|
}
|
|
1642
1668
|
|
|
1669
|
+
function readDeferredParentComponentProps(el) {
|
|
1670
|
+
var serialized = el.getAttribute('data-stx-deferred-parent-bindings');
|
|
1671
|
+
if (!serialized) return {};
|
|
1672
|
+
try {
|
|
1673
|
+
var parsed = JSON.parse(serialized);
|
|
1674
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
1675
|
+
}
|
|
1676
|
+
catch (e) {
|
|
1677
|
+
return {};
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function preserveDeferredParentComponentProps(el) {
|
|
1682
|
+
var names = (el.getAttribute('data-stx-parent-bindings') || '').split(/\\s+/).filter(Boolean);
|
|
1683
|
+
if (!names.length) return;
|
|
1684
|
+
var deferred = readDeferredParentComponentProps(el);
|
|
1685
|
+
names.forEach(function(name) {
|
|
1686
|
+
var sourceName = ':' + name;
|
|
1687
|
+
if (el.hasAttribute(sourceName))
|
|
1688
|
+
deferred[name] = el.getAttribute(sourceName);
|
|
1689
|
+
});
|
|
1690
|
+
if (Object.keys(deferred).length)
|
|
1691
|
+
el.setAttribute('data-stx-deferred-parent-bindings', JSON.stringify(deferred));
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function hasPendingLoopBoundary(el) {
|
|
1695
|
+
var loopBoundary = el;
|
|
1696
|
+
while (loopBoundary && loopBoundary !== document) {
|
|
1697
|
+
var loopExpression = loopBoundary.getAttribute && (
|
|
1698
|
+
loopBoundary.getAttribute('@for')
|
|
1699
|
+
|| loopBoundary.getAttribute(':for')
|
|
1700
|
+
|| loopBoundary.getAttribute('x-for')
|
|
1701
|
+
);
|
|
1702
|
+
if (loopExpression && isForDirectiveExpression(loopExpression))
|
|
1703
|
+
return true;
|
|
1704
|
+
loopBoundary = loopBoundary.parentElement || loopBoundary.parentNode;
|
|
1705
|
+
}
|
|
1706
|
+
return false;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function hasPendingAncestorLoopBoundary(el) {
|
|
1710
|
+
var loopBoundary = el && (el.parentElement || el.parentNode);
|
|
1711
|
+
while (loopBoundary && loopBoundary !== document) {
|
|
1712
|
+
var loopExpression = loopBoundary.getAttribute && (
|
|
1713
|
+
loopBoundary.getAttribute('@for')
|
|
1714
|
+
|| loopBoundary.getAttribute(':for')
|
|
1715
|
+
|| loopBoundary.getAttribute('x-for')
|
|
1716
|
+
);
|
|
1717
|
+
if (loopExpression && isForDirectiveExpression(loopExpression))
|
|
1718
|
+
return true;
|
|
1719
|
+
loopBoundary = loopBoundary.parentElement || loopBoundary.parentNode;
|
|
1720
|
+
}
|
|
1721
|
+
return false;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function syncComponentPropSignal(el, name, value) {
|
|
1725
|
+
if (!el || !el.getAttribute || value === undefined) return;
|
|
1726
|
+
var scopeId = el.getAttribute('data-stx-scope');
|
|
1727
|
+
var scopeVars = scopeId && window.stx._scopes && window.stx._scopes[scopeId];
|
|
1728
|
+
if (!scopeVars) return;
|
|
1729
|
+
var camelName = name.replace(/-([a-z0-9])/g, function(_, character) {
|
|
1730
|
+
return character.toUpperCase();
|
|
1731
|
+
});
|
|
1732
|
+
var propSignal = scopeVars[name] || scopeVars[camelName];
|
|
1733
|
+
if (!propSignal || !propSignal._isSignal || typeof propSignal.set !== 'function') return;
|
|
1734
|
+
var directValues = el.__stx_direct_prop_values || (el.__stx_direct_prop_values = {});
|
|
1735
|
+
var directValue = {
|
|
1736
|
+
raw: el.hasAttribute(name) ? el.getAttribute(name) : null,
|
|
1737
|
+
value: value,
|
|
1738
|
+
};
|
|
1739
|
+
directValues[name] = directValue;
|
|
1740
|
+
directValues[camelName] = directValue;
|
|
1741
|
+
// This runs inside the parent binding effect. Reading the child prop
|
|
1742
|
+
// signal normally would subscribe that effect to its own output. Parent
|
|
1743
|
+
// expressions that return a fresh array or object would then recurse:
|
|
1744
|
+
// syncing the child retriggers the parent, which creates another value.
|
|
1745
|
+
// Compare without dependency tracking so the effect only follows the
|
|
1746
|
+
// caller-owned signals used by the binding expression.
|
|
1747
|
+
if (!Object.is(peek(function() { return propSignal(); }), value))
|
|
1748
|
+
propSignal.set(value);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1643
1751
|
function bindParentComponentProps(el, callerScope) {
|
|
1644
1752
|
if (!el || !el.getAttribute || el.__stx_parent_props_bound) return;
|
|
1645
1753
|
var names = (el.getAttribute('data-stx-parent-bindings') || '').split(/\\s+/).filter(Boolean);
|
|
1646
1754
|
if (!names.length) return;
|
|
1755
|
+
if (hasPendingLoopBoundary(el)) {
|
|
1756
|
+
preserveDeferredParentComponentProps(el);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
var deferred = readDeferredParentComponentProps(el);
|
|
1760
|
+
var bindings = names.map(function(name) {
|
|
1761
|
+
var sourceName = ':' + name;
|
|
1762
|
+
var expression = el.hasAttribute(sourceName) ? el.getAttribute(sourceName) : deferred[name];
|
|
1763
|
+
return typeof expression === 'string'
|
|
1764
|
+
? { name: name, sourceName: sourceName, expression: expression }
|
|
1765
|
+
: null;
|
|
1766
|
+
}).filter(Boolean);
|
|
1767
|
+
if (!bindings.length) return;
|
|
1647
1768
|
el.__stx_parent_props_bound = true;
|
|
1648
1769
|
var scope = { ...globalHelpers, ...callerScope };
|
|
1649
1770
|
|
|
1650
|
-
|
|
1651
|
-
var
|
|
1652
|
-
|
|
1653
|
-
var expression =
|
|
1771
|
+
bindings.forEach(function(binding) {
|
|
1772
|
+
var name = binding.name;
|
|
1773
|
+
var sourceName = binding.sourceName;
|
|
1774
|
+
var expression = binding.expression;
|
|
1654
1775
|
effect(function() {
|
|
1655
1776
|
var value;
|
|
1656
1777
|
try {
|
|
@@ -1687,9 +1808,21 @@ catch (e) {
|
|
|
1687
1808
|
}
|
|
1688
1809
|
el.setAttribute(name, serialized);
|
|
1689
1810
|
}
|
|
1811
|
+
// The component setup scope is already registered for normal and
|
|
1812
|
+
// cloned signal components. Update its prop signal directly so object
|
|
1813
|
+
// identity and synchronous Vue-like prop delivery do not depend on a
|
|
1814
|
+
// MutationObserver round trip through a serialized DOM attribute.
|
|
1815
|
+
// The attribute remains the public DOM reflection and observer fallback
|
|
1816
|
+
// for scopes that register after this binding pass.
|
|
1817
|
+
syncComponentPropSignal(el, name, value);
|
|
1690
1818
|
});
|
|
1691
1819
|
el.removeAttribute(sourceName);
|
|
1820
|
+
delete deferred[name];
|
|
1692
1821
|
});
|
|
1822
|
+
if (Object.keys(deferred).length)
|
|
1823
|
+
el.setAttribute('data-stx-deferred-parent-bindings', JSON.stringify(deferred));
|
|
1824
|
+
else
|
|
1825
|
+
el.removeAttribute('data-stx-deferred-parent-bindings');
|
|
1693
1826
|
}
|
|
1694
1827
|
|
|
1695
1828
|
function resolveComponentCallerScope(el, pageScope) {
|
|
@@ -1720,11 +1853,69 @@ catch (e) {
|
|
|
1720
1853
|
root.querySelectorAll('[data-stx-scope]').forEach(function(scopeEl) { scopeEls.push(scopeEl); });
|
|
1721
1854
|
scopeEls.forEach(function(scopeEl) {
|
|
1722
1855
|
if (scopeEl.__stx_parent_props_bound) return;
|
|
1856
|
+
// A component inside a descendant loop cannot resolve its caller props
|
|
1857
|
+
// until bindFor creates the iteration scope. Binding it while the outer
|
|
1858
|
+
// conditional is being detached would consume expressions such as
|
|
1859
|
+
// :status="deployment.status" before the deployment item exists, leaving every
|
|
1860
|
+
// later clone with the child's default prop. bindFor hydrates these
|
|
1861
|
+
// component scopes after insertion with the correct per-item scope.
|
|
1862
|
+
var current = scopeEl;
|
|
1863
|
+
while (current && current !== root) {
|
|
1864
|
+
if (current.hasAttribute
|
|
1865
|
+
&& (current.hasAttribute('@for') || current.hasAttribute(':for') || current.hasAttribute('x-for'))) {
|
|
1866
|
+
preserveDeferredParentComponentProps(scopeEl);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
current = current.parentElement || current.parentNode;
|
|
1870
|
+
}
|
|
1723
1871
|
scopeEl.__stx_parent_scope = resolveComponentCallerScope(scopeEl, callerScope);
|
|
1724
1872
|
bindParentComponentProps(scopeEl, scopeEl.__stx_parent_scope);
|
|
1725
1873
|
});
|
|
1726
1874
|
}
|
|
1727
1875
|
|
|
1876
|
+
// Component setup scripts are emitted immediately after their scoped root.
|
|
1877
|
+
// When that root itself carries :if, detaching only the element leaves the
|
|
1878
|
+
// sibling setup script outside the conditional node group. Scripts inserted
|
|
1879
|
+
// through innerHTML are inert, so the later hydration pass has no script to
|
|
1880
|
+
// execute and the component keeps raw directives and interpolations.
|
|
1881
|
+
function findConditionalComponentSetupSiblings(root) {
|
|
1882
|
+
if (!root || !root.hasAttribute || !root.hasAttribute('data-stx-scope')) return [];
|
|
1883
|
+
var scopeId = root.getAttribute('data-stx-scope');
|
|
1884
|
+
var templateScopeId = root.getAttribute('data-stx-template-scope') || scopeId;
|
|
1885
|
+
if (!scopeId) return [];
|
|
1886
|
+
|
|
1887
|
+
var scripts = [];
|
|
1888
|
+
var sibling = root.nextSibling;
|
|
1889
|
+
while (sibling) {
|
|
1890
|
+
var next = sibling.nextSibling;
|
|
1891
|
+
if (sibling.nodeType === Node.TEXT_NODE && !(sibling.textContent || '').trim()) {
|
|
1892
|
+
sibling = next;
|
|
1893
|
+
continue;
|
|
1894
|
+
}
|
|
1895
|
+
if (sibling.nodeType === Node.COMMENT_NODE) {
|
|
1896
|
+
sibling = next;
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
if (sibling.nodeType === Node.ELEMENT_NODE
|
|
1900
|
+
&& sibling.tagName === 'STYLE'
|
|
1901
|
+
&& sibling.hasAttribute('data-stx-vendor')) {
|
|
1902
|
+
sibling = next;
|
|
1903
|
+
continue;
|
|
1904
|
+
}
|
|
1905
|
+
if (sibling.nodeType === Node.ELEMENT_NODE
|
|
1906
|
+
&& sibling.tagName === 'SCRIPT'
|
|
1907
|
+
&& sibling.hasAttribute('data-stx-scoped')
|
|
1908
|
+
&& ((sibling.textContent || '').indexOf(scopeId) !== -1
|
|
1909
|
+
|| (templateScopeId && (sibling.textContent || '').indexOf(templateScopeId) !== -1))) {
|
|
1910
|
+
scripts.push(sibling);
|
|
1911
|
+
sibling = next;
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
break;
|
|
1915
|
+
}
|
|
1916
|
+
return scripts;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1728
1919
|
function processElement(el, scope = componentScope) {
|
|
1729
1920
|
// Debug: log every element with x-class or @click
|
|
1730
1921
|
if (el.nodeType === Node.ELEMENT_NODE && el.hasAttribute) {
|
|
@@ -1739,6 +1930,17 @@ catch (e) {
|
|
|
1739
1930
|
deferHydration(el, trigger, scope);
|
|
1740
1931
|
return;
|
|
1741
1932
|
}
|
|
1933
|
+
if (el.nodeType === Node.ELEMENT_NODE
|
|
1934
|
+
&& el.hasAttribute
|
|
1935
|
+
&& el.hasAttribute('data-stx-scope')
|
|
1936
|
+
&& hasPendingAncestorLoopBoundary(el)) {
|
|
1937
|
+
preserveDeferredParentComponentProps(el);
|
|
1938
|
+
// Even when the component is itself a loop root, an ancestor loop must
|
|
1939
|
+
// create its caller item scope first. The ancestor's bindFor pass will
|
|
1940
|
+
// clone and hydrate this component with that scope, then its own loop can
|
|
1941
|
+
// evaluate expressions such as group.resources.
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1742
1944
|
|
|
1743
1945
|
if (el.nodeType === Node.ELEMENT_NODE && el.tagName === 'BUTTON' && el.hasAttribute && el.hasAttribute('@click')) {
|
|
1744
1946
|
|
|
@@ -1964,9 +2166,9 @@ catch (e2) {
|
|
|
1964
2166
|
|
|
1965
2167
|
// Handle attributes
|
|
1966
2168
|
if (el.hasAttribute && (el.hasAttribute('x-class') || el.hasAttribute('@click'))) {
|
|
1967
|
-
console.log('[stx] attr loop entry:', el.tagName, 'attrs:',
|
|
2169
|
+
console.log('[stx] attr loop entry:', el.tagName, 'attrs:', getElementAttributes(el).map(a => a.name).join(', '));
|
|
1968
2170
|
}
|
|
1969
|
-
|
|
2171
|
+
getElementAttributes(el).forEach(attr => {
|
|
1970
2172
|
const name = attr.name;
|
|
1971
2173
|
const value = attr.value;
|
|
1972
2174
|
if (!name) return;
|
|
@@ -2008,6 +2210,7 @@ catch (e2) {
|
|
|
2008
2210
|
el.value = desiredValue;
|
|
2009
2211
|
if (desiredValue === '' && el.tagName !== 'OPTION') el.removeAttribute(attrName);
|
|
2010
2212
|
else if (el.getAttribute(attrName) !== desiredValue) el.setAttribute(attrName, desiredValue);
|
|
2213
|
+
if (typeof el.__stx_model_sync === 'function') el.__stx_model_sync();
|
|
2011
2214
|
};
|
|
2012
2215
|
applyControlValue();
|
|
2013
2216
|
|
|
@@ -2032,6 +2235,24 @@ catch (e2) {
|
|
|
2032
2235
|
}
|
|
2033
2236
|
return;
|
|
2034
2237
|
}
|
|
2238
|
+
// Checked, selected, and indeterminate are live DOM properties, just like value.
|
|
2239
|
+
// Updating only the HTML attribute leaves a user-touched checkbox or
|
|
2240
|
+
// radio in its previous state because the property becomes dirty.
|
|
2241
|
+
// Indeterminate has no corresponding effective HTML attribute at all.
|
|
2242
|
+
if ((attrName === 'checked' || attrName === 'selected' || attrName === 'indeterminate') && attrName in el) {
|
|
2243
|
+
var selectedState = Boolean(v);
|
|
2244
|
+
el[attrName] = selectedState;
|
|
2245
|
+
if (attrName === 'indeterminate') {
|
|
2246
|
+
el.removeAttribute(attrName);
|
|
2247
|
+
}
|
|
2248
|
+
else if (selectedState) {
|
|
2249
|
+
el.setAttribute(attrName, '');
|
|
2250
|
+
}
|
|
2251
|
+
else {
|
|
2252
|
+
el.removeAttribute(attrName);
|
|
2253
|
+
}
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2035
2256
|
if (v === false || v === null || v === undefined) {
|
|
2036
2257
|
el.removeAttribute(attrName);
|
|
2037
2258
|
}
|
|
@@ -2076,6 +2297,14 @@ else if (name === 'ref' || name === ':ref' || name === 'x-ref' || name === 'data
|
|
|
2076
2297
|
// Store ref in scope.$refs and componentScope.$refs
|
|
2077
2298
|
if (scope.$refs) scope.$refs[value] = el;
|
|
2078
2299
|
if (componentScope.$refs) componentScope.$refs[value] = el;
|
|
2300
|
+
// Also fill a same-named signal declared in the script block, so the
|
|
2301
|
+
// documented ref() + x-ref pair works and reading the signal returns
|
|
2302
|
+
// the element. Without this the signal stayed empty and every read of
|
|
2303
|
+
// it silently returned undefined.
|
|
2304
|
+
var refTarget = (scope && scope[value]) || componentScope[value];
|
|
2305
|
+
if (refTarget && typeof refTarget === 'function' && refTarget._isSignal && typeof refTarget.set === 'function') {
|
|
2306
|
+
refTarget.set(el);
|
|
2307
|
+
}
|
|
2079
2308
|
}
|
|
2080
2309
|
else if (name.startsWith('@') || name.startsWith(':')) {
|
|
2081
2310
|
// Event handlers: @click, :click, @submit.prevent, :keydown.enter, etc.
|
|
@@ -2103,11 +2332,15 @@ else if (name === 'ref' || name === ':ref' || name === 'x-ref' || name === 'data
|
|
|
2103
2332
|
console.log('[stx] binding event:', eventName, 'on', el.tagName, 'expr:', value.substring(0, 40));
|
|
2104
2333
|
el.addEventListener(eventName, (event) => {
|
|
2105
2334
|
console.log('[stx] event fired:', eventName, 'on', el.tagName);
|
|
2106
|
-
//
|
|
2107
|
-
//
|
|
2108
|
-
//
|
|
2109
|
-
//
|
|
2110
|
-
|
|
2335
|
+
// Vue-style component listeners fall through to the rendered native
|
|
2336
|
+
// root unless the child emits the same event. Signal components use
|
|
2337
|
+
// a scope wrapper, so native events arrive here from a descendant.
|
|
2338
|
+
// Ignore that native event only after defineEmits() has declared or
|
|
2339
|
+
// emitted the same name, preventing submit/click handlers from
|
|
2340
|
+
// firing once for the native event and again for the CustomEvent.
|
|
2341
|
+
var componentEmits = el.__stx_component_emits;
|
|
2342
|
+
if (isForwardedComponentEvent && event.target !== el
|
|
2343
|
+
&& componentEmits && componentEmits[eventName]) return;
|
|
2111
2344
|
|
|
2112
2345
|
// Vue-style component handlers receive the emitted payload directly.
|
|
2113
2346
|
// Native root events still receive the Event object so server-only
|
|
@@ -2172,7 +2405,16 @@ else if (name === 'ref' || name === ':ref' || name === 'x-ref' || name === 'data
|
|
|
2172
2405
|
// primitive reads.
|
|
2173
2406
|
var unwrapVars = Object.keys(eventCapturedScope).map(function(k) {
|
|
2174
2407
|
var v = eventCapturedScope[k];
|
|
2175
|
-
|
|
2408
|
+
// A :for item is a signal only as an implementation detail, so
|
|
2409
|
+
// that keyed rows can update in place. Its \`.value\` is a field
|
|
2410
|
+
// on the item, never the signal compatibility API \u2014 the same
|
|
2411
|
+
// rule createAutoUnwrapProxy states for binding expressions.
|
|
2412
|
+
// Without the carve-out, expressionUsesSignalApi sees
|
|
2413
|
+
// \`option.value\` and hands the handler the raw signal, so a
|
|
2414
|
+
// handler like \`pick(option.value)\` receives the whole item.
|
|
2415
|
+
// It is not an error, so it lands silently: the wrong value is
|
|
2416
|
+
// stored and only surfaces much later.
|
|
2417
|
+
if (v && typeof v === 'function' && v._isSignal && !v._isStxLoopItem && expressionUsesSignalApi(value, k)) {
|
|
2176
2418
|
return 'var ' + k + ' = __s["' + k + '"]';
|
|
2177
2419
|
}
|
|
2178
2420
|
return 'var ' + k + ' = __s["' + k + '"] && typeof __s["' + k + '"] === "function" && __s["' + k + '"]._isSignal ? __s["' + k + '"]() : __s["' + k + '"]';
|
|
@@ -2379,12 +2621,54 @@ catch (e) {
|
|
|
2379
2621
|
}
|
|
2380
2622
|
};
|
|
2381
2623
|
|
|
2382
|
-
if (tag === 'input' &&
|
|
2383
|
-
|
|
2384
|
-
|
|
2624
|
+
if (tag === 'input' && type === 'checkbox') {
|
|
2625
|
+
var syncCheckbox = () => {
|
|
2626
|
+
var current = getValue();
|
|
2627
|
+
el.checked = Array.isArray(current)
|
|
2628
|
+
? current.some(function(item) { return String(item) === String(el.value); })
|
|
2629
|
+
: Boolean(current);
|
|
2630
|
+
};
|
|
2631
|
+
el.__stx_model_sync = syncCheckbox;
|
|
2632
|
+
effect(syncCheckbox);
|
|
2633
|
+
el.addEventListener('change', () => {
|
|
2634
|
+
var current = getValue();
|
|
2635
|
+
if (!Array.isArray(current)) {
|
|
2636
|
+
setValue(el.checked);
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
var checkboxValue = coerceValue(el.value);
|
|
2641
|
+
var next = current.filter(function(item) { return String(item) !== String(checkboxValue); });
|
|
2642
|
+
if (el.checked) next.push(checkboxValue);
|
|
2643
|
+
setValue(next);
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
else if (tag === 'input' && type === 'radio') {
|
|
2647
|
+
var syncRadio = () => { el.checked = String(getValue()) === String(el.value); };
|
|
2648
|
+
el.__stx_model_sync = syncRadio;
|
|
2649
|
+
effect(syncRadio);
|
|
2650
|
+
el.addEventListener('change', () => {
|
|
2651
|
+
if (el.checked) setValue(coerceValue(el.value));
|
|
2652
|
+
});
|
|
2385
2653
|
}
|
|
2386
2654
|
else if (tag === 'select') {
|
|
2387
|
-
|
|
2655
|
+
var syncSelect = () => { el.value = getValue() ?? ''; };
|
|
2656
|
+
el.__stx_model_sync = syncSelect;
|
|
2657
|
+
effect(syncSelect);
|
|
2658
|
+
if (!el.__stx_model_observer && typeof window.MutationObserver !== 'undefined') {
|
|
2659
|
+
el.__stx_model_observer = new window.MutationObserver(syncSelect);
|
|
2660
|
+
el.__stx_model_observer.observe(el, {
|
|
2661
|
+
childList: true,
|
|
2662
|
+
subtree: true,
|
|
2663
|
+
attributes: true,
|
|
2664
|
+
attributeFilter: ['value'],
|
|
2665
|
+
});
|
|
2666
|
+
el.__stx_destroy = el.__stx_destroy || [];
|
|
2667
|
+
el.__stx_destroy.push(function() {
|
|
2668
|
+
if (el.__stx_model_observer) el.__stx_model_observer.disconnect();
|
|
2669
|
+
el.__stx_model_observer = null;
|
|
2670
|
+
});
|
|
2671
|
+
}
|
|
2388
2672
|
el.addEventListener('change', () => setValue(coerceValue(el.value)));
|
|
2389
2673
|
}
|
|
2390
2674
|
else {
|
|
@@ -2591,6 +2875,179 @@ else if (typeof value === 'string') {
|
|
|
2591
2875
|
});
|
|
2592
2876
|
}
|
|
2593
2877
|
|
|
2878
|
+
var __forComponentScopeCounter = 0;
|
|
2879
|
+
var __forComponentSetupFactories = new Map();
|
|
2880
|
+
|
|
2881
|
+
function forEachGroupElement(nodes, callback) {
|
|
2882
|
+
nodes.forEach(function(node) {
|
|
2883
|
+
if (!node || node.nodeType !== Node.ELEMENT_NODE) return;
|
|
2884
|
+
callback(node);
|
|
2885
|
+
if (node.querySelectorAll) {
|
|
2886
|
+
node.querySelectorAll('*').forEach(function(child) { callback(child); });
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
// A signal component expanded inside <template :for> carries a compiled
|
|
2892
|
+
// data-stx-scope id and setup script. cloneNode() used to duplicate that id
|
|
2893
|
+
// for every row, so all instances shared the first child's props and state.
|
|
2894
|
+
// Give each cloned component tree its own ids before insertion, and rewrite
|
|
2895
|
+
// the setup scripts that target those ids.
|
|
2896
|
+
function remapForComponentScopes(nodes) {
|
|
2897
|
+
var scopeMap = {};
|
|
2898
|
+
forEachGroupElement(nodes, function(node) {
|
|
2899
|
+
if (!node.hasAttribute || !node.hasAttribute('data-stx-scope')) return;
|
|
2900
|
+
var oldId = node.getAttribute('data-stx-scope');
|
|
2901
|
+
if (!oldId) return;
|
|
2902
|
+
if (!scopeMap[oldId])
|
|
2903
|
+
scopeMap[oldId] = oldId + '_for_' + (++__forComponentScopeCounter);
|
|
2904
|
+
// Keep the compiler's original id through any additional structural
|
|
2905
|
+
// clones. DOM cloneNode() preserves attributes but not expando fields.
|
|
2906
|
+
if (!node.hasAttribute('data-stx-template-scope'))
|
|
2907
|
+
node.setAttribute('data-stx-template-scope', oldId);
|
|
2908
|
+
node.setAttribute('data-stx-scope', scopeMap[oldId]);
|
|
2909
|
+
});
|
|
2910
|
+
|
|
2911
|
+
var oldIds = Object.keys(scopeMap);
|
|
2912
|
+
if (!oldIds.length) return;
|
|
2913
|
+
|
|
2914
|
+
forEachGroupElement(nodes, function(node) {
|
|
2915
|
+
if (node.attributes) {
|
|
2916
|
+
Array.from(node.attributes).forEach(function(rawAttribute) {
|
|
2917
|
+
var attribute = Array.isArray(rawAttribute)
|
|
2918
|
+
? { name: rawAttribute[0], value: rawAttribute[1] }
|
|
2919
|
+
: rawAttribute;
|
|
2920
|
+
if (!attribute || attribute.name === 'data-stx-scope' || attribute.name === 'data-stx-template-scope') return;
|
|
2921
|
+
var value = attribute.value;
|
|
2922
|
+
if (typeof value !== 'string') return;
|
|
2923
|
+
oldIds.forEach(function(oldId) {
|
|
2924
|
+
if (value.indexOf(oldId) !== -1)
|
|
2925
|
+
value = value.split(oldId).join(scopeMap[oldId]);
|
|
2926
|
+
});
|
|
2927
|
+
if (value !== attribute.value) node.setAttribute(attribute.name, value);
|
|
2928
|
+
});
|
|
2929
|
+
}
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
function hydrateComponentScopes(nodes, callerScope) {
|
|
2934
|
+
var roots = [];
|
|
2935
|
+
var scripts = [];
|
|
2936
|
+
forEachGroupElement(nodes, function(node) {
|
|
2937
|
+
if (node.hasAttribute && node.hasAttribute('data-stx-scope')) roots.push(node);
|
|
2938
|
+
if (node.tagName === 'SCRIPT' && node.hasAttribute('data-stx-scoped')) scripts.push(node);
|
|
2939
|
+
});
|
|
2940
|
+
if (!roots.length) return;
|
|
2941
|
+
|
|
2942
|
+
function hasPendingStructuralAncestor(root) {
|
|
2943
|
+
var cursor = root && root.parentElement;
|
|
2944
|
+
while (cursor && cursor.nodeType === Node.ELEMENT_NODE) {
|
|
2945
|
+
if (cursor.hasAttribute('@for')
|
|
2946
|
+
|| cursor.hasAttribute(':for')
|
|
2947
|
+
|| cursor.hasAttribute('x-for')
|
|
2948
|
+
|| cursor.hasAttribute('@if')
|
|
2949
|
+
|| cursor.hasAttribute(':if')
|
|
2950
|
+
|| cursor.hasAttribute('x-if')
|
|
2951
|
+
|| cursor.hasAttribute('@else-if')
|
|
2952
|
+
|| cursor.hasAttribute(':else-if')
|
|
2953
|
+
|| cursor.hasAttribute('x-else-if')
|
|
2954
|
+
|| cursor.hasAttribute('@else')
|
|
2955
|
+
|| cursor.hasAttribute(':else')
|
|
2956
|
+
|| cursor.hasAttribute('x-else'))
|
|
2957
|
+
return true;
|
|
2958
|
+
cursor = cursor.parentElement;
|
|
2959
|
+
}
|
|
2960
|
+
return false;
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// Scripts cloned from template.content are inert in browsers. Compile each
|
|
2964
|
+
// distinct component setup body once, then run that cached function for
|
|
2965
|
+
// every loop instance. Temporarily restoring the template's original scope
|
|
2966
|
+
// id lets existing compiled setup code locate the correct root without
|
|
2967
|
+
// rewriting and reparsing the full script for every row.
|
|
2968
|
+
scripts.forEach(function(old) {
|
|
2969
|
+
var text = old.textContent || '';
|
|
2970
|
+
var pendingRoots = roots.filter(function(root) {
|
|
2971
|
+
var scopeId = root.getAttribute('data-stx-scope');
|
|
2972
|
+
var originalScopeId = root.getAttribute('data-stx-template-scope') || scopeId;
|
|
2973
|
+
return scopeId
|
|
2974
|
+
&& originalScopeId
|
|
2975
|
+
&& text.indexOf(originalScopeId) !== -1
|
|
2976
|
+
&& !hasPendingStructuralAncestor(root)
|
|
2977
|
+
&& !(window.stx._scopes && window.stx._scopes[scopeId]);
|
|
2978
|
+
});
|
|
2979
|
+
if (!pendingRoots.length) return;
|
|
2980
|
+
|
|
2981
|
+
var setup = __forComponentSetupFactories.get(text);
|
|
2982
|
+
if (!setup) {
|
|
2983
|
+
try {
|
|
2984
|
+
setup = new Function(text);
|
|
2985
|
+
__forComponentSetupFactories.set(text, setup);
|
|
2986
|
+
}
|
|
2987
|
+
catch (e) {
|
|
2988
|
+
console.error('[stx] cloned component setup compile error:', e);
|
|
2989
|
+
return;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
pendingRoots.forEach(function(root) {
|
|
2994
|
+
var scopeId = root.getAttribute('data-stx-scope');
|
|
2995
|
+
var originalScopeId = root.getAttribute('data-stx-template-scope') || scopeId;
|
|
2996
|
+
if (!scopeId || !originalScopeId) return;
|
|
2997
|
+
|
|
2998
|
+
root.setAttribute('data-stx-scope', originalScopeId);
|
|
2999
|
+
try {
|
|
3000
|
+
setup();
|
|
3001
|
+
var registeredScope = window.stx._scopes && (window.stx._scopes[originalScopeId] || window.stx._scopes[scopeId]);
|
|
3002
|
+
if (registeredScope && originalScopeId !== scopeId) {
|
|
3003
|
+
delete window.stx._scopes[originalScopeId];
|
|
3004
|
+
window.stx._scopes[scopeId] = registeredScope;
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
catch (e) {
|
|
3008
|
+
console.error('[stx] cloned component setup error:', e);
|
|
3009
|
+
}
|
|
3010
|
+
finally {
|
|
3011
|
+
root.setAttribute('data-stx-scope', scopeId);
|
|
3012
|
+
}
|
|
3013
|
+
});
|
|
3014
|
+
|
|
3015
|
+
// A component that is itself a structural source still needs its setup
|
|
3016
|
+
// sibling when bindFor/bindIf turns that source into a clone template.
|
|
3017
|
+
// Final row instances have no structural attribute and can discard it.
|
|
3018
|
+
var isStructuralSource = pendingRoots.some(function(root) {
|
|
3019
|
+
return root.hasAttribute('@for')
|
|
3020
|
+
|| root.hasAttribute(':for')
|
|
3021
|
+
|| root.hasAttribute('x-for')
|
|
3022
|
+
|| root.hasAttribute('@if')
|
|
3023
|
+
|| root.hasAttribute(':if')
|
|
3024
|
+
|| root.hasAttribute('x-if');
|
|
3025
|
+
});
|
|
3026
|
+
if (!isStructuralSource && old.parentNode) old.parentNode.removeChild(old);
|
|
3027
|
+
});
|
|
3028
|
+
|
|
3029
|
+
roots.forEach(function(root) {
|
|
3030
|
+
var scopeId = root.getAttribute('data-stx-scope');
|
|
3031
|
+
var scopeVars = scopeId && window.stx._scopes && window.stx._scopes[scopeId];
|
|
3032
|
+
var resolvedCallerScope = resolveComponentCallerScope(root, callerScope);
|
|
3033
|
+
root.__stx_parent_scope = resolvedCallerScope;
|
|
3034
|
+
bindParentComponentProps(root, resolvedCallerScope);
|
|
3035
|
+
if (!scopeVars || root.__stx_disposers) return;
|
|
3036
|
+
scopeVars.__el = root;
|
|
3037
|
+
var childScope = { ...resolvedCallerScope, ...scopeVars };
|
|
3038
|
+
root.__stx_disposers = trackEffects(function() {
|
|
3039
|
+
processElement(root, childScope);
|
|
3040
|
+
});
|
|
3041
|
+
if (scopeVars.__mountCallbacks && !scopeVars.__mounted) {
|
|
3042
|
+
scopeVars.__mounted = true;
|
|
3043
|
+
scopeVars.__mountCallbacks.forEach(function(fn) {
|
|
3044
|
+
try { fn(); }
|
|
3045
|
+
catch (e) { console.error('[stx] onMount error:', e); }
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
|
|
2594
3051
|
function bindFor(el, passedScope = componentScope, attrName = '@for') {
|
|
2595
3052
|
if (el.__stx_for_bound) return;
|
|
2596
3053
|
el.__stx_for_bound = true;
|
|
@@ -2614,6 +3071,34 @@ else if (typeof value === 'string') {
|
|
|
2614
3071
|
|
|
2615
3072
|
const placeholder = document.createComment('stx-for');
|
|
2616
3073
|
const isTemplate = el.tagName === 'TEMPLATE';
|
|
3074
|
+
const componentSetupTemplates = [];
|
|
3075
|
+
if (!isTemplate && el.hasAttribute('data-stx-scope')) {
|
|
3076
|
+
const componentScopeId = el.getAttribute('data-stx-scope');
|
|
3077
|
+
const componentTemplateScopeId = el.getAttribute('data-stx-template-scope') || componentScopeId;
|
|
3078
|
+
let setupSibling = el.nextSibling;
|
|
3079
|
+
while (setupSibling) {
|
|
3080
|
+
const nextSetupSibling = setupSibling.nextSibling;
|
|
3081
|
+
if (setupSibling.nodeType === Node.TEXT_NODE && !(setupSibling.textContent || '').trim()) {
|
|
3082
|
+
setupSibling = nextSetupSibling;
|
|
3083
|
+
continue;
|
|
3084
|
+
}
|
|
3085
|
+
if (setupSibling.nodeType === Node.COMMENT_NODE) {
|
|
3086
|
+
setupSibling = nextSetupSibling;
|
|
3087
|
+
continue;
|
|
3088
|
+
}
|
|
3089
|
+
if (setupSibling.nodeType === Node.ELEMENT_NODE
|
|
3090
|
+
&& setupSibling.tagName === 'SCRIPT'
|
|
3091
|
+
&& setupSibling.hasAttribute('data-stx-scoped')
|
|
3092
|
+
&& ((setupSibling.textContent || '').indexOf(componentScopeId) !== -1
|
|
3093
|
+
|| (componentTemplateScopeId && (setupSibling.textContent || '').indexOf(componentTemplateScopeId) !== -1))) {
|
|
3094
|
+
componentSetupTemplates.push(setupSibling.cloneNode(true));
|
|
3095
|
+
setupSibling.remove();
|
|
3096
|
+
setupSibling = nextSetupSibling;
|
|
3097
|
+
continue;
|
|
3098
|
+
}
|
|
3099
|
+
break;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
2617
3102
|
|
|
2618
3103
|
// Check if element also has @if / :if - need to handle together
|
|
2619
3104
|
const ifExpr = el.getAttribute('@if') || el.getAttribute(':if');
|
|
@@ -2650,6 +3135,11 @@ else if (typeof value === 'string') {
|
|
|
2650
3135
|
const capturedScope = findElementScope(el) || findElementScope(parent);
|
|
2651
3136
|
|
|
2652
3137
|
parent.insertBefore(placeholder, el);
|
|
3138
|
+
// Component setup scripts execute while the server-rendered loop source is
|
|
3139
|
+
// still in the document. That source becomes a template, not a live
|
|
3140
|
+
// instance, so reclaim all registered scopes before removing it. Every
|
|
3141
|
+
// rendered iteration receives remapped setup scripts and fresh scopes.
|
|
3142
|
+
disposeSubtreeScopes(el);
|
|
2653
3143
|
parent.removeChild(el);
|
|
2654
3144
|
|
|
2655
3145
|
// Capture :key expression BEFORE removing the attribute
|
|
@@ -2686,6 +3176,49 @@ else {
|
|
|
2686
3176
|
let loadingElement = null;
|
|
2687
3177
|
let emptyElement = null;
|
|
2688
3178
|
|
|
3179
|
+
// A <template :for> can contain another structural directive. That
|
|
3180
|
+
// nested directive replaces its source node with a placeholder and owns
|
|
3181
|
+
// additional live siblings around that placeholder. Track an explicit
|
|
3182
|
+
// boundary for every template iteration so keyed reuse moves the complete
|
|
3183
|
+
// live range, including nodes created by nested :for / :if bindings.
|
|
3184
|
+
// Moving only the original cloned nodes pulled static siblings past the
|
|
3185
|
+
// nested range on the next outer update (header, then rows became rows,
|
|
3186
|
+
// then header).
|
|
3187
|
+
const liveGroupNodes = (group) => {
|
|
3188
|
+
if (!isTemplate || !group || group.length < 2) return group || [];
|
|
3189
|
+
const start = group[0];
|
|
3190
|
+
const end = group[group.length - 1];
|
|
3191
|
+
if (!start || !end || start.parentNode !== parent || end.parentNode !== parent)
|
|
3192
|
+
return group.filter(groupNode => groupNode && groupNode.parentNode === parent);
|
|
3193
|
+
|
|
3194
|
+
const nodes = [];
|
|
3195
|
+
let currentNode = start;
|
|
3196
|
+
while (currentNode) {
|
|
3197
|
+
nodes.push(currentNode);
|
|
3198
|
+
if (currentNode === end) break;
|
|
3199
|
+
currentNode = currentNode.nextSibling;
|
|
3200
|
+
}
|
|
3201
|
+
return nodes;
|
|
3202
|
+
};
|
|
3203
|
+
|
|
3204
|
+
const removeGroup = (group) => {
|
|
3205
|
+
const liveNodes = liveGroupNodes(group);
|
|
3206
|
+
const liveSet = new Set(liveNodes);
|
|
3207
|
+
liveNodes.forEach(e => { disposeSubtreeScopes(e); e.remove(); });
|
|
3208
|
+
group.forEach(groupNode => {
|
|
3209
|
+
if (liveSet.has(groupNode)) return;
|
|
3210
|
+
disposeSubtreeScopes(groupNode);
|
|
3211
|
+
groupNode.remove();
|
|
3212
|
+
});
|
|
3213
|
+
};
|
|
3214
|
+
|
|
3215
|
+
const clearRenderedItems = () => {
|
|
3216
|
+
currentGroups.forEach(removeGroup);
|
|
3217
|
+
currentElements = [];
|
|
3218
|
+
currentGroups = [];
|
|
3219
|
+
currentKeys = [];
|
|
3220
|
+
};
|
|
3221
|
+
|
|
2689
3222
|
// Helper to evaluate with captured scope (auto-unwraps signals)
|
|
2690
3223
|
// NOTE: This eagerly reads ALL signals in scope \u2014 use evalLazy inside effects
|
|
2691
3224
|
// to avoid over-broad dependency tracking.
|
|
@@ -2779,6 +3312,7 @@ catch (e) {
|
|
|
2779
3312
|
// The item is wrapped in a signal so bindings track it reactively.
|
|
2780
3313
|
const createItemElements = (item, index, key) => {
|
|
2781
3314
|
const itemSignal = state(item);
|
|
3315
|
+
itemSignal._isStxLoopItem = true;
|
|
2782
3316
|
const indexSignal = state(index);
|
|
2783
3317
|
if (key) itemSignalMap.set(key, { item: itemSignal, index: indexSignal });
|
|
2784
3318
|
|
|
@@ -2788,6 +3322,7 @@ catch (e) {
|
|
|
2788
3322
|
|
|
2789
3323
|
const elements = [];
|
|
2790
3324
|
if (isTemplate) {
|
|
3325
|
+
elements.push(document.createComment('stx-for-item-start'));
|
|
2791
3326
|
Array.from(templateContent.childNodes).forEach(node => {
|
|
2792
3327
|
const clone = node.cloneNode(true);
|
|
2793
3328
|
if (clone.nodeType === 1) {
|
|
@@ -2797,11 +3332,16 @@ catch (e) {
|
|
|
2797
3332
|
}
|
|
2798
3333
|
elements.push(clone);
|
|
2799
3334
|
});
|
|
3335
|
+
elements.push(document.createComment('stx-for-item-end'));
|
|
2800
3336
|
} else {
|
|
2801
3337
|
const clone = templateContent.cloneNode(true);
|
|
2802
3338
|
clone.__stx_for_scope = itemScope;
|
|
2803
3339
|
elements.push(clone);
|
|
3340
|
+
componentSetupTemplates.forEach(function(setupTemplate) {
|
|
3341
|
+
elements.push(setupTemplate.cloneNode(true));
|
|
3342
|
+
});
|
|
2804
3343
|
}
|
|
3344
|
+
remapForComponentScopes(elements);
|
|
2805
3345
|
return elements;
|
|
2806
3346
|
};
|
|
2807
3347
|
|
|
@@ -2827,10 +3367,7 @@ catch (e) {
|
|
|
2827
3367
|
const isLoading = evalLazy(loadingExpr);
|
|
2828
3368
|
if (isLoading) {
|
|
2829
3369
|
// Dispose scopes registered by items before they leave the DOM (#1727).
|
|
2830
|
-
|
|
2831
|
-
currentElements = [];
|
|
2832
|
-
currentGroups = [];
|
|
2833
|
-
currentKeys = [];
|
|
3370
|
+
clearRenderedItems();
|
|
2834
3371
|
showLoading();
|
|
2835
3372
|
return;
|
|
2836
3373
|
}
|
|
@@ -2876,10 +3413,7 @@ catch (e) {
|
|
|
2876
3413
|
const ifValue = evalLazy(ifExpr);
|
|
2877
3414
|
if (!ifValue) {
|
|
2878
3415
|
// Dispose scopes registered by items before they leave the DOM (#1727).
|
|
2879
|
-
|
|
2880
|
-
currentElements = [];
|
|
2881
|
-
currentGroups = [];
|
|
2882
|
-
currentKeys = [];
|
|
3416
|
+
clearRenderedItems();
|
|
2883
3417
|
hideEmpty();
|
|
2884
3418
|
return;
|
|
2885
3419
|
}
|
|
@@ -2917,10 +3451,7 @@ catch (e) {
|
|
|
2917
3451
|
// Check empty state (Feature #3)
|
|
2918
3452
|
if (list.length === 0) {
|
|
2919
3453
|
// Dispose scopes registered by items before they leave the DOM (#1727).
|
|
2920
|
-
|
|
2921
|
-
currentElements = [];
|
|
2922
|
-
currentGroups = [];
|
|
2923
|
-
currentKeys = [];
|
|
3454
|
+
clearRenderedItems();
|
|
2924
3455
|
if (emptyExpr) {
|
|
2925
3456
|
const emptyContent = evalLazy(emptyExpr);
|
|
2926
3457
|
if (emptyContent && typeof emptyContent === 'string') {
|
|
@@ -2973,9 +3504,10 @@ catch (e) {
|
|
|
2973
3504
|
// group's nodes. Its own placeholder owns reinsertion, so keyed reuse
|
|
2974
3505
|
// must not resurrect that hidden node.
|
|
2975
3506
|
const group = existing.shift();
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
3507
|
+
const liveNodes = liveGroupNodes(group);
|
|
3508
|
+
const fragment = document.createDocumentFragment();
|
|
3509
|
+
liveNodes.forEach(groupNode => fragment.appendChild(groupNode));
|
|
3510
|
+
parent.insertBefore(fragment, placeholder);
|
|
2979
3511
|
newElements.push(...group);
|
|
2980
3512
|
newGroups.push(group);
|
|
2981
3513
|
usedKeys.add(key);
|
|
@@ -2992,14 +3524,20 @@ catch (e) {
|
|
|
2992
3524
|
const elements = createItemElements(list[i], i, key);
|
|
2993
3525
|
elements.forEach(el => {
|
|
2994
3526
|
parent.insertBefore(el, placeholder);
|
|
3527
|
+
if (tgReady && el.isConnected) tgEnter(el, tgName);
|
|
3528
|
+
});
|
|
3529
|
+
hydrateComponentScopes(elements, elements.find(function(node) {
|
|
3530
|
+
return node && node.__stx_for_scope;
|
|
3531
|
+
})?.__stx_for_scope || passedScope);
|
|
3532
|
+
elements.forEach(el => {
|
|
2995
3533
|
if (el.nodeType === 1) {
|
|
2996
3534
|
var itemScope = el.__stx_for_scope || passedScope;
|
|
2997
3535
|
delete el.__stx_for_scope;
|
|
2998
|
-
|
|
3536
|
+
if (!el.__stx_disposers && el.tagName !== 'SCRIPT')
|
|
3537
|
+
processElement(el, itemScope);
|
|
2999
3538
|
el.removeAttribute('x-cloak');
|
|
3000
3539
|
el.querySelectorAll('[x-cloak]').forEach(c => c.removeAttribute('x-cloak'));
|
|
3001
3540
|
}
|
|
3002
|
-
if (tgReady && el.isConnected) tgEnter(el, tgName);
|
|
3003
3541
|
});
|
|
3004
3542
|
newElements.push(...elements);
|
|
3005
3543
|
newGroups.push(elements);
|
|
@@ -3011,11 +3549,28 @@ catch (e) {
|
|
|
3011
3549
|
// remove() call (#1727).
|
|
3012
3550
|
for (const [key, groups] of oldKeyMap) {
|
|
3013
3551
|
if (!usedKeys.has(key)) {
|
|
3014
|
-
const elements = groups.flat();
|
|
3015
3552
|
// In a transition group, run the leave animation and let it remove the
|
|
3016
3553
|
// node; otherwise remove immediately. Scopes are disposed up-front
|
|
3017
3554
|
// either way (their destroy hooks shouldn't wait on a CSS transition).
|
|
3018
|
-
|
|
3555
|
+
groups.forEach((group) => {
|
|
3556
|
+
if (tgReady) {
|
|
3557
|
+
const liveNodes = liveGroupNodes(group);
|
|
3558
|
+
const transitioned = new Set();
|
|
3559
|
+
liveNodes.forEach((el) => {
|
|
3560
|
+
disposeSubtreeScopes(el);
|
|
3561
|
+
if (tgLeave(el, tgName)) transitioned.add(el);
|
|
3562
|
+
else el.remove();
|
|
3563
|
+
});
|
|
3564
|
+
group.forEach((el) => {
|
|
3565
|
+
if (transitioned.has(el) || liveNodes.includes(el)) return;
|
|
3566
|
+
disposeSubtreeScopes(el);
|
|
3567
|
+
el.remove();
|
|
3568
|
+
});
|
|
3569
|
+
}
|
|
3570
|
+
else {
|
|
3571
|
+
removeGroup(group);
|
|
3572
|
+
}
|
|
3573
|
+
});
|
|
3019
3574
|
itemSignalMap.delete(key);
|
|
3020
3575
|
}
|
|
3021
3576
|
}
|
|
@@ -3214,9 +3769,26 @@ catch (e2) {
|
|
|
3214
3769
|
(function (branch) {
|
|
3215
3770
|
setTimeout(function () {
|
|
3216
3771
|
var childScope = { ...globalHelpers, ...capturedComponentScope, ...(branch.capturedElementScope || {}) };
|
|
3772
|
+
// Components compiled inside any branch of an if/else-if/else
|
|
3773
|
+
// chain carry scoped setup scripts with them. Branch nodes are
|
|
3774
|
+
// detached before those scripts can execute, so simply
|
|
3775
|
+
// re-inserting the selected branch leaves component props,
|
|
3776
|
+
// events, directives, and projected slot expressions inert.
|
|
3777
|
+
// Hydrate those scopes before the normal subtree walk, matching
|
|
3778
|
+
// the single-template :if and :for insertion paths.
|
|
3779
|
+
var previouslyTrackedNodes = new Set();
|
|
3780
|
+
branch.nodes.forEach(function (n) {
|
|
3781
|
+
if (n && n.__stx_disposers) previouslyTrackedNodes.add(n);
|
|
3782
|
+
});
|
|
3783
|
+
hydrateComponentScopes(branch.nodes, childScope);
|
|
3217
3784
|
branch.nodes.forEach(function (n) {
|
|
3218
3785
|
if (n.nodeType !== 1) return; // whitespace/text between template children
|
|
3219
|
-
|
|
3786
|
+
// A scoped component receives its disposer in the outer scope
|
|
3787
|
+
// walk immediately after processElement returns from binding
|
|
3788
|
+
// this conditional. That first pass intentionally stops at
|
|
3789
|
+
// :if, so a disposer does not mean its children were hydrated.
|
|
3790
|
+
if (previouslyTrackedNodes.has(n) || !n.__stx_disposers)
|
|
3791
|
+
processElement(n, childScope);
|
|
3220
3792
|
n.removeAttribute('x-cloak');
|
|
3221
3793
|
n.querySelectorAll('[x-cloak]').forEach(function (c) { c.removeAttribute('x-cloak'); });
|
|
3222
3794
|
});
|
|
@@ -3245,10 +3817,11 @@ catch (e2) {
|
|
|
3245
3817
|
|
|
3246
3818
|
const placeholder = document.createComment('stx-if');
|
|
3247
3819
|
let isInserted = true;
|
|
3248
|
-
let currentNodes = [];
|
|
3249
3820
|
|
|
3250
3821
|
// Handle <template> elements specially - clone their content
|
|
3251
3822
|
const isTemplate = el.tagName === 'TEMPLATE';
|
|
3823
|
+
const componentSetupSiblings = isTemplate ? [] : findConditionalComponentSetupSiblings(el);
|
|
3824
|
+
let currentNodes = isTemplate ? [] : [el, ...componentSetupSiblings];
|
|
3252
3825
|
|
|
3253
3826
|
// Capture BOTH element scope AND passedScope NOW before anything changes
|
|
3254
3827
|
// passedScope may contain @for iteration variables or parent component signals
|
|
@@ -3323,6 +3896,28 @@ catch (e2) {
|
|
|
3323
3896
|
childrenProcessed = true;
|
|
3324
3897
|
};
|
|
3325
3898
|
|
|
3899
|
+
// A <template :if> inserts clones of template.content rather than the
|
|
3900
|
+
// template element itself. Those clones still need the normal directive
|
|
3901
|
+
// and interpolation pass. Previously the initially-visible branch was
|
|
3902
|
+
// inserted and left inert, so bindings such as :text and {{ expression }}
|
|
3903
|
+
// leaked into the rendered UI until the condition toggled.
|
|
3904
|
+
const processTemplateNodes = (nodes) => {
|
|
3905
|
+
const childScope = { ...globalHelpers, ...capturedComponentScope, ...(capturedElementScope || {}) };
|
|
3906
|
+
// Components compiled inside a <template :if> carry setup scripts in the
|
|
3907
|
+
// template content. Browser-cloned scripts are inert, so register their
|
|
3908
|
+
// scopes and forwarded props before walking the inserted branch. This is
|
|
3909
|
+
// the same component hydration path used by <template :for>.
|
|
3910
|
+
hydrateComponentScopes(nodes, childScope);
|
|
3911
|
+
nodes.forEach(node => {
|
|
3912
|
+
if (!node.__stx_disposers && node.tagName !== 'SCRIPT')
|
|
3913
|
+
processElement(node, childScope);
|
|
3914
|
+
if (node.nodeType === 1) {
|
|
3915
|
+
node.removeAttribute('x-cloak');
|
|
3916
|
+
node.querySelectorAll('[x-cloak]').forEach(c => c.removeAttribute('x-cloak'));
|
|
3917
|
+
}
|
|
3918
|
+
});
|
|
3919
|
+
};
|
|
3920
|
+
|
|
3326
3921
|
// Evaluate the :if expression \u2014 use direct signal read for simple refs,
|
|
3327
3922
|
// falling back to evalExpr for complex expressions
|
|
3328
3923
|
const fullScope = { ...globalHelpers, ...capturedComponentScope, ...(capturedElementScope || {}) };
|
|
@@ -3351,16 +3946,8 @@ catch (e2) {
|
|
|
3351
3946
|
currentNodes = Array.from(el.content.childNodes).map(n => n.cloneNode(true));
|
|
3352
3947
|
const insertionAnchor = placeholder.nextSibling;
|
|
3353
3948
|
currentNodes.forEach(node => parent.insertBefore(node, insertionAnchor));
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
currentNodes.forEach(node => {
|
|
3357
|
-
if (node.nodeType === 1) {
|
|
3358
|
-
processElement(node, childScope);
|
|
3359
|
-
// Remove x-cloak from processed clones
|
|
3360
|
-
node.removeAttribute('x-cloak');
|
|
3361
|
-
node.querySelectorAll('[x-cloak]').forEach(c => c.removeAttribute('x-cloak'));
|
|
3362
|
-
}
|
|
3363
|
-
});
|
|
3949
|
+
processTemplateNodes(currentNodes);
|
|
3950
|
+
childrenProcessed = true;
|
|
3364
3951
|
// Stamp insertion time for click propagation guard
|
|
3365
3952
|
currentNodes.forEach(function(n) { if (n.nodeType === 1) n.__stx_shown_at = performance.now(); });
|
|
3366
3953
|
isInserted = true;
|
|
@@ -3372,12 +3959,22 @@ else if (!value && isInserted) {
|
|
|
3372
3959
|
currentNodes.forEach(node => node.remove());
|
|
3373
3960
|
currentNodes = [];
|
|
3374
3961
|
isInserted = false;
|
|
3962
|
+
childrenProcessed = false;
|
|
3963
|
+
}
|
|
3964
|
+
else if (value && isInserted && !childrenProcessed) {
|
|
3965
|
+
// Initial template content is cloned before the effect runs. Process
|
|
3966
|
+
// it after the condition is confirmed so hidden branches stay inert.
|
|
3967
|
+
childrenProcessed = true;
|
|
3968
|
+
setTimeout(function() {
|
|
3969
|
+
if (isInserted) processTemplateNodes(currentNodes);
|
|
3970
|
+
}, 0);
|
|
3375
3971
|
}
|
|
3376
3972
|
}
|
|
3377
3973
|
else {
|
|
3378
3974
|
if (value && !isInserted) {
|
|
3379
3975
|
console.log('[stx] bindIf INSERTING element for :if=' + expr);
|
|
3380
|
-
|
|
3976
|
+
const insertionAnchor = placeholder.nextSibling;
|
|
3977
|
+
currentNodes.forEach(node => parent.insertBefore(node, insertionAnchor));
|
|
3381
3978
|
el.__stx_shown_at = performance.now();
|
|
3382
3979
|
isInserted = true;
|
|
3383
3980
|
}
|
|
@@ -3392,7 +3989,7 @@ else if (!value && isInserted) {
|
|
|
3392
3989
|
// once (#1737). Permanent disposal stays in bindFor (item removal)
|
|
3393
3990
|
// and cleanupContainer (SPA navigation). The double-bind guards make
|
|
3394
3991
|
// re-show idempotent, so toggling doesn't leak.
|
|
3395
|
-
|
|
3992
|
+
currentNodes.forEach(node => node.remove());
|
|
3396
3993
|
isInserted = false;
|
|
3397
3994
|
console.log('[stx] bindIf REMOVED, el.isConnected:', el.isConnected);
|
|
3398
3995
|
}
|
|
@@ -3407,7 +4004,15 @@ else if (!value && isInserted) {
|
|
|
3407
4004
|
childrenProcessed = true;
|
|
3408
4005
|
setTimeout(function() {
|
|
3409
4006
|
var childScope = { ...globalHelpers, ...capturedComponentScope, ...(capturedElementScope || {}) };
|
|
3410
|
-
|
|
4007
|
+
// A false single-element conditional is detached before the
|
|
4008
|
+
// DOMContentLoaded component-scope pass. When it becomes visible,
|
|
4009
|
+
// hydrate any component roots in the restored subtree first so
|
|
4010
|
+
// projected slot bindings and component-local directives receive
|
|
4011
|
+
// the same lifecycle as components inserted by template :if.
|
|
4012
|
+
var wasTrackedBeforeConditionalHydration = !!el.__stx_disposers;
|
|
4013
|
+
hydrateComponentScopes(currentNodes, childScope);
|
|
4014
|
+
if (wasTrackedBeforeConditionalHydration || !el.__stx_disposers)
|
|
4015
|
+
processElement(el, childScope);
|
|
3411
4016
|
// Remove x-cloak from the inserted subtree \u2014 the initial
|
|
3412
4017
|
// cloak removal (after processElement on the root) already
|
|
3413
4018
|
// ran before this deferred processing, so newly-inserted
|
|
@@ -3542,6 +4147,15 @@ else if (timer === null) {
|
|
|
3542
4147
|
}
|
|
3543
4148
|
|
|
3544
4149
|
function useInterval(interval, options) {
|
|
4150
|
+
// Accept the callback-first form too \u2014 useInterval(fn, 1000) is what most
|
|
4151
|
+
// callers reach for, and silently ignoring the callback (the previous
|
|
4152
|
+
// behaviour) produced a timer that ticked into the void.
|
|
4153
|
+
var callback = null;
|
|
4154
|
+
if (typeof interval === 'function') {
|
|
4155
|
+
callback = interval;
|
|
4156
|
+
interval = typeof options === 'number' ? options : 1000;
|
|
4157
|
+
options = {};
|
|
4158
|
+
}
|
|
3545
4159
|
interval = interval || 1000;
|
|
3546
4160
|
options = options || {};
|
|
3547
4161
|
var count = 0;
|
|
@@ -3569,6 +4183,7 @@ else if (timer === null) {
|
|
|
3569
4183
|
listeners.forEach(function(fn) { fn(count); });
|
|
3570
4184
|
resume();
|
|
3571
4185
|
}
|
|
4186
|
+
if (callback) listeners.push(callback);
|
|
3572
4187
|
resume();
|
|
3573
4188
|
onDestroy(function() { pause(); listeners = []; });
|
|
3574
4189
|
return {
|
|
@@ -3659,8 +4274,16 @@ else if (timer === null) {
|
|
|
3659
4274
|
}
|
|
3660
4275
|
|
|
3661
4276
|
function useClickOutside(target, handler) {
|
|
4277
|
+
function resolveTarget() {
|
|
4278
|
+
if (typeof target === 'string') return document.querySelector(target);
|
|
4279
|
+
if (target && typeof target === 'object') {
|
|
4280
|
+
if ('current' in target) return target.current;
|
|
4281
|
+
if ('value' in target) return target.value;
|
|
4282
|
+
}
|
|
4283
|
+
return target;
|
|
4284
|
+
}
|
|
3662
4285
|
function listener(event) {
|
|
3663
|
-
var el =
|
|
4286
|
+
var el = resolveTarget();
|
|
3664
4287
|
if (!el) return;
|
|
3665
4288
|
if (el === event.target || el.contains(event.target)) return;
|
|
3666
4289
|
handler(event);
|
|
@@ -3707,6 +4330,15 @@ else if (timer === null) {
|
|
|
3707
4330
|
};
|
|
3708
4331
|
}
|
|
3709
4332
|
|
|
4333
|
+
// Queue work after the current synchronous signal/effect flush. This mirrors
|
|
4334
|
+
// the Composition API nextTick contract and gives template refs a chance to
|
|
4335
|
+
// resolve after a structural directive inserts their elements.
|
|
4336
|
+
function nextTick(fn) {
|
|
4337
|
+
return Promise.resolve().then(function() {
|
|
4338
|
+
if (fn) fn();
|
|
4339
|
+
});
|
|
4340
|
+
}
|
|
4341
|
+
|
|
3710
4342
|
function useAsync(fn, options) {
|
|
3711
4343
|
options = options || {};
|
|
3712
4344
|
var asyncState = 'idle';
|
|
@@ -3845,6 +4477,61 @@ catch (e) {}
|
|
|
3845
4477
|
return matches;
|
|
3846
4478
|
}
|
|
3847
4479
|
|
|
4480
|
+
var scrollLockStates = new WeakMap();
|
|
4481
|
+
|
|
4482
|
+
function resolveScrollLockTarget(target) {
|
|
4483
|
+
var value = target;
|
|
4484
|
+
if (typeof value === 'function') value = value();
|
|
4485
|
+
if (value && typeof value === 'object' && 'current' in value) value = value.current;
|
|
4486
|
+
if (value && typeof value === 'object' && !value.nodeType && 'value' in value) value = value.value;
|
|
4487
|
+
return value || document.body || document.documentElement;
|
|
4488
|
+
}
|
|
4489
|
+
|
|
4490
|
+
function acquireScrollLock(element) {
|
|
4491
|
+
if (!element) return;
|
|
4492
|
+
var existing = scrollLockStates.get(element);
|
|
4493
|
+
if (existing) {
|
|
4494
|
+
existing.count += 1;
|
|
4495
|
+
return;
|
|
4496
|
+
}
|
|
4497
|
+
scrollLockStates.set(element, { count: 1, overflow: element.style.overflow });
|
|
4498
|
+
element.style.overflow = 'hidden';
|
|
4499
|
+
}
|
|
4500
|
+
|
|
4501
|
+
function releaseScrollLock(element) {
|
|
4502
|
+
if (!element) return;
|
|
4503
|
+
var existing = scrollLockStates.get(element);
|
|
4504
|
+
if (!existing) return;
|
|
4505
|
+
existing.count -= 1;
|
|
4506
|
+
if (existing.count > 0) return;
|
|
4507
|
+
element.style.overflow = existing.overflow;
|
|
4508
|
+
scrollLockStates.delete(element);
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
function useScrollLock(target) {
|
|
4512
|
+
var locked = state(false);
|
|
4513
|
+
var lockedElement = null;
|
|
4514
|
+
var unsubscribe = locked.subscribe(function(value) {
|
|
4515
|
+
if (value) {
|
|
4516
|
+
if (lockedElement) return;
|
|
4517
|
+
lockedElement = resolveScrollLockTarget(target);
|
|
4518
|
+
acquireScrollLock(lockedElement);
|
|
4519
|
+
}
|
|
4520
|
+
else if (lockedElement) {
|
|
4521
|
+
releaseScrollLock(lockedElement);
|
|
4522
|
+
lockedElement = null;
|
|
4523
|
+
}
|
|
4524
|
+
});
|
|
4525
|
+
onDestroy(function() {
|
|
4526
|
+
unsubscribe();
|
|
4527
|
+
if (lockedElement) {
|
|
4528
|
+
releaseScrollLock(lockedElement);
|
|
4529
|
+
lockedElement = null;
|
|
4530
|
+
}
|
|
4531
|
+
});
|
|
4532
|
+
return locked;
|
|
4533
|
+
}
|
|
4534
|
+
|
|
3848
4535
|
function usePreferredDark() { return useMediaQuery('(prefers-color-scheme: dark)'); }
|
|
3849
4536
|
function usePreferredLight() { return useMediaQuery('(prefers-color-scheme: light)'); }
|
|
3850
4537
|
function usePreferredReducedMotion() { return useMediaQuery('(prefers-reduced-motion: reduce)'); }
|
|
@@ -3932,6 +4619,30 @@ catch (e) {}
|
|
|
3932
4619
|
return s;
|
|
3933
4620
|
}
|
|
3934
4621
|
|
|
4622
|
+
var __stxGeneratedIdCounter = 0;
|
|
4623
|
+
|
|
4624
|
+
// Stable, scope-aware DOM ids for component authoring. A component setup
|
|
4625
|
+
// runs with window.__STX_CURRENT_ELEMENT__ pointing at its compiled root, so
|
|
4626
|
+
// the data-stx scope id is already the canonical per-instance identity. The
|
|
4627
|
+
// local counter supports multiple useId() calls in one component while the
|
|
4628
|
+
// global fallback covers page scripts and manually-mounted roots.
|
|
4629
|
+
function useId(prefix) {
|
|
4630
|
+
var normalizedPrefix = String(prefix || 'stx')
|
|
4631
|
+
.trim()
|
|
4632
|
+
.replace(/[^a-zA-Z0-9_-]+/g, '-')
|
|
4633
|
+
.replace(/^-+|-+$/g, '') || 'stx';
|
|
4634
|
+
var root = window.__STX_CURRENT_ELEMENT__;
|
|
4635
|
+
if (root && root.getAttribute) {
|
|
4636
|
+
var scopeId = root.getAttribute('data-stx-scope') || root.getAttribute('data-stx') || '';
|
|
4637
|
+
if (scopeId) {
|
|
4638
|
+
root.__stx_id_counter = (root.__stx_id_counter || 0) + 1;
|
|
4639
|
+
return normalizedPrefix + '-' + String(scopeId).replace(/[^a-zA-Z0-9_-]+/g, '-') + '-' + root.__stx_id_counter;
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
__stxGeneratedIdCounter++;
|
|
4643
|
+
return normalizedPrefix + '-' + __stxGeneratedIdCounter;
|
|
4644
|
+
}
|
|
4645
|
+
|
|
3935
4646
|
// Reactive prop binding. Bridges the gap between a parent's clientReactive
|
|
3936
4647
|
// attribute (e.g. :open="modalOpen()" on a component) and the component's
|
|
3937
4648
|
// internal state. Returns a signal whose value tracks the named attribute
|
|
@@ -4018,7 +4729,19 @@ catch (e) {}
|
|
|
4018
4729
|
var raw = attribute.raw;
|
|
4019
4730
|
if (raw === lastRaw) return;
|
|
4020
4731
|
lastRaw = raw;
|
|
4021
|
-
var
|
|
4732
|
+
var directValues = root.__stx_direct_prop_values;
|
|
4733
|
+
var directValue = directValues && directValues[name];
|
|
4734
|
+
var next;
|
|
4735
|
+
if (directValue && directValue.raw === raw) {
|
|
4736
|
+
next = directValue.value;
|
|
4737
|
+
delete directValues[name];
|
|
4738
|
+
for (var directKey in directValues) {
|
|
4739
|
+
if (directValues[directKey] === directValue) delete directValues[directKey];
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4742
|
+
else {
|
|
4743
|
+
next = hasAttr ? parse(raw) : staticValue;
|
|
4744
|
+
}
|
|
4022
4745
|
if (next !== s()) s.set(next);
|
|
4023
4746
|
});
|
|
4024
4747
|
observer.observe(root, { attributes: true, attributeFilter: attributeNames });
|
|
@@ -4561,16 +5284,19 @@ catch (e) {}
|
|
|
4561
5284
|
useCounter,
|
|
4562
5285
|
useClickOutside,
|
|
4563
5286
|
useFocus,
|
|
5287
|
+
nextTick,
|
|
4564
5288
|
useAsync,
|
|
4565
5289
|
useLocalStorage,
|
|
4566
5290
|
useSessionStorage,
|
|
4567
5291
|
useCookie,
|
|
5292
|
+
useId,
|
|
4568
5293
|
useReactiveProp,
|
|
4569
5294
|
useEventListener,
|
|
4570
5295
|
useWebSocket,
|
|
4571
5296
|
useColorMode,
|
|
4572
5297
|
useDark,
|
|
4573
5298
|
useMediaQuery,
|
|
5299
|
+
useScrollLock,
|
|
4574
5300
|
usePreferredDark,
|
|
4575
5301
|
usePreferredLight,
|
|
4576
5302
|
usePreferredReducedMotion,
|
|
@@ -4615,10 +5341,23 @@ catch (e) {}
|
|
|
4615
5341
|
}
|
|
4616
5342
|
return result;
|
|
4617
5343
|
},
|
|
4618
|
-
defineEmits: function() {
|
|
5344
|
+
defineEmits: function(events) {
|
|
4619
5345
|
var el = window.__STX_CURRENT_ELEMENT__ || null;
|
|
5346
|
+
var declaredEvents = Array.isArray(events)
|
|
5347
|
+
? events
|
|
5348
|
+
: (events && typeof events === 'object' ? Object.keys(events) : []);
|
|
5349
|
+
if (el && declaredEvents.length) {
|
|
5350
|
+
el.__stx_component_emits = el.__stx_component_emits || Object.create(null);
|
|
5351
|
+
declaredEvents.forEach(function(event) {
|
|
5352
|
+
el.__stx_component_emits[event] = true;
|
|
5353
|
+
});
|
|
5354
|
+
}
|
|
4620
5355
|
return function(event, payload) {
|
|
4621
5356
|
var target = el || document.body;
|
|
5357
|
+
if (el) {
|
|
5358
|
+
el.__stx_component_emits = el.__stx_component_emits || Object.create(null);
|
|
5359
|
+
el.__stx_component_emits[event] = true;
|
|
5360
|
+
}
|
|
4622
5361
|
target.dispatchEvent(new CustomEvent(event, { detail: payload, bubbles: true, cancelable: true }));
|
|
4623
5362
|
};
|
|
4624
5363
|
},
|
|
@@ -5075,6 +5814,7 @@ else {
|
|
|
5075
5814
|
window.useCounter = useCounter;
|
|
5076
5815
|
window.useClickOutside = useClickOutside;
|
|
5077
5816
|
window.useFocus = useFocus;
|
|
5817
|
+
window.nextTick = nextTick;
|
|
5078
5818
|
window.useAsync = useAsync;
|
|
5079
5819
|
window.useLocalStorage = useLocalStorage;
|
|
5080
5820
|
window.useEventListener = useEventListener;
|