@jsswift/ui 1.0.52 → 1.0.54
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/css/responsive-base.css +557 -0
- package/dist/css/ui-light.css +9936 -0
- package/dist/css/ui-light.min.css +1 -0
- package/dist/min-ui.js +3 -3
- package/dist/ui.js +154 -68
- package/dist/ui.min.js +3 -3
- package/package.json +4 -2
- package/src/10-primitives-layout.js +147 -65
- package/src/80-data.js +7 -3
- package/src/css/README.md +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsswift/ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.54",
|
|
4
4
|
"description": "JSswift UI layer built on top of @jsswift/core",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"./img/svg/tabler-icons-sprite.svg": "./dist/img/svg/tabler-icons-sprite.svg",
|
|
24
24
|
"./css/base.css": "./dist/css/base.css",
|
|
25
25
|
"./css/ui.css": "./dist/css/ui.css",
|
|
26
|
+
"./css/ui-light.css": "./dist/css/ui-light.css",
|
|
27
|
+
"./css/ui-light.min.css": "./dist/css/ui-light.min.css",
|
|
26
28
|
"./css/min-ui.css": "./dist/css/min-ui.css",
|
|
27
29
|
"./css/ui.min.css": "./dist/css/ui.min.css",
|
|
28
30
|
"./css/ui-components.css": "./dist/css/ui-components.css",
|
|
@@ -38,6 +40,6 @@
|
|
|
38
40
|
"src"
|
|
39
41
|
],
|
|
40
42
|
"peerDependencies": {
|
|
41
|
-
"@jsswift/core": "^1.0.
|
|
43
|
+
"@jsswift/core": "^1.0.54"
|
|
42
44
|
}
|
|
43
45
|
}
|
|
@@ -1310,7 +1310,8 @@
|
|
|
1310
1310
|
|| typeof noteSlot === "function"
|
|
1311
1311
|
|| typeof hintSlot === "function";
|
|
1312
1312
|
if (canReact) {
|
|
1313
|
-
JSswift.reactive.effect(() => { renderMessage(); }, "UI.FormField:message");
|
|
1313
|
+
const stopMessage = JSswift.reactive.effect(() => { renderMessage(); }, "UI.FormField:message");
|
|
1314
|
+
JSswift._registerCleanup(wrap, stopMessage);
|
|
1314
1315
|
} else {
|
|
1315
1316
|
renderMessage();
|
|
1316
1317
|
}
|
|
@@ -1333,6 +1334,16 @@
|
|
|
1333
1334
|
value: props.value
|
|
1334
1335
|
});
|
|
1335
1336
|
|
|
1337
|
+
let autofillDisposed = false;
|
|
1338
|
+
const autofillTimeouts = new Set();
|
|
1339
|
+
const autofillLater = (fn, delay) => {
|
|
1340
|
+
if (autofillDisposed) return;
|
|
1341
|
+
const id = setTimeout(() => {
|
|
1342
|
+
autofillTimeouts.delete(id);
|
|
1343
|
+
if (!autofillDisposed) fn();
|
|
1344
|
+
}, delay);
|
|
1345
|
+
autofillTimeouts.add(id);
|
|
1346
|
+
};
|
|
1336
1347
|
let lastKnownValue = el.value ?? "";
|
|
1337
1348
|
const syncAutofill = () => {
|
|
1338
1349
|
const next = el.value ?? "";
|
|
@@ -1344,7 +1355,7 @@
|
|
|
1344
1355
|
let autofillTimer = null;
|
|
1345
1356
|
let autofillProbe = false;
|
|
1346
1357
|
const scheduleAutofillProbe = () => {
|
|
1347
|
-
if (autofillProbe) return;
|
|
1358
|
+
if (autofillDisposed || autofillProbe) return;
|
|
1348
1359
|
autofillProbe = true;
|
|
1349
1360
|
let count = 0;
|
|
1350
1361
|
const tick = () => {
|
|
@@ -1354,12 +1365,12 @@
|
|
|
1354
1365
|
autofillProbe = false;
|
|
1355
1366
|
return;
|
|
1356
1367
|
}
|
|
1357
|
-
|
|
1368
|
+
autofillLater(tick, 120);
|
|
1358
1369
|
};
|
|
1359
|
-
|
|
1370
|
+
autofillLater(tick, 40);
|
|
1360
1371
|
};
|
|
1361
1372
|
const startAutofillWatch = () => {
|
|
1362
|
-
if (autofillTimer) return;
|
|
1373
|
+
if (autofillDisposed || autofillTimer) return;
|
|
1363
1374
|
autofillTimer = setInterval(() => {
|
|
1364
1375
|
if (document.activeElement !== el) {
|
|
1365
1376
|
clearInterval(autofillTimer);
|
|
@@ -1385,27 +1396,33 @@
|
|
|
1385
1396
|
el.addEventListener("focus", () => {
|
|
1386
1397
|
startAutofillWatch();
|
|
1387
1398
|
scheduleAutofillProbe();
|
|
1388
|
-
|
|
1399
|
+
autofillLater(syncAutofill, 50);
|
|
1389
1400
|
});
|
|
1390
1401
|
el.addEventListener("blur", () => {
|
|
1391
1402
|
stopAutofillWatch();
|
|
1392
|
-
|
|
1403
|
+
autofillLater(syncAutofill, 0);
|
|
1393
1404
|
});
|
|
1394
|
-
|
|
1405
|
+
autofillLater(syncAutofill, 0);
|
|
1395
1406
|
scheduleAutofillProbe();
|
|
1396
1407
|
|
|
1408
|
+
JSswift._registerCleanup(el, () => {
|
|
1409
|
+
autofillDisposed = true;
|
|
1410
|
+
stopAutofillWatch();
|
|
1411
|
+
autofillTimeouts.forEach(clearTimeout);
|
|
1412
|
+
autofillTimeouts.clear();
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1397
1415
|
// model binding
|
|
1398
1416
|
const model = props.model;
|
|
1399
1417
|
if (model) {
|
|
1400
1418
|
// Supporta rod direttamente
|
|
1401
1419
|
if (typeof model === "object" && typeof model._bind === "function") {
|
|
1402
|
-
app.rodModel(el, model);
|
|
1420
|
+
JSswift._registerCleanup(el, app.rodModel(el, model));
|
|
1403
1421
|
}
|
|
1404
1422
|
// Supporta [get,set] signal -> creiamo rodFromSignal
|
|
1405
1423
|
else if (Array.isArray(model) && typeof model[0] === "function" && typeof model[1] === "function") {
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
// NOTE: se vuoi cleanup automatico qui, lo facciamo nel layer component (v2)
|
|
1424
|
+
const stop = JSswift.reactive.untracked(() => app.signalModel(el, model[0], model[1]));
|
|
1425
|
+
JSswift._registerCleanup(el, stop);
|
|
1409
1426
|
}
|
|
1410
1427
|
}
|
|
1411
1428
|
uiRegisterShortcode(el, props, {
|
|
@@ -1422,7 +1439,7 @@
|
|
|
1422
1439
|
if (value == null || value === false) control.removeAttribute(name);
|
|
1423
1440
|
else control.setAttribute(name, String(value));
|
|
1424
1441
|
};
|
|
1425
|
-
JSswift.reactive.effect(() => {
|
|
1442
|
+
const stopAttributes = JSswift.reactive.effect(() => {
|
|
1426
1443
|
const label = uiUnwrap(props.ariaLabel) ?? (typeof props.label === "string" ? props.label : null);
|
|
1427
1444
|
set("aria-label", label);
|
|
1428
1445
|
set("aria-labelledby", uiUnwrap(props.ariaLabelledBy));
|
|
@@ -1437,6 +1454,7 @@
|
|
|
1437
1454
|
set(name.toLowerCase(), uiUnwrap(props[name]));
|
|
1438
1455
|
}
|
|
1439
1456
|
}, "UI.Field:controlAttributes");
|
|
1457
|
+
JSswift._registerCleanup(field, stopAttributes);
|
|
1440
1458
|
}
|
|
1441
1459
|
|
|
1442
1460
|
UI.Input = (props = {}) => {
|
|
@@ -1456,6 +1474,16 @@
|
|
|
1456
1474
|
readOnly: !!props.readonly
|
|
1457
1475
|
});
|
|
1458
1476
|
|
|
1477
|
+
let autofillDisposed = false;
|
|
1478
|
+
const autofillTimeouts = new Set();
|
|
1479
|
+
const autofillLater = (fn, delay) => {
|
|
1480
|
+
if (autofillDisposed) return;
|
|
1481
|
+
const id = setTimeout(() => {
|
|
1482
|
+
autofillTimeouts.delete(id);
|
|
1483
|
+
if (!autofillDisposed) fn();
|
|
1484
|
+
}, delay);
|
|
1485
|
+
autofillTimeouts.add(id);
|
|
1486
|
+
};
|
|
1459
1487
|
let lastKnownValue = input.value ?? "";
|
|
1460
1488
|
const syncAutofill = () => {
|
|
1461
1489
|
const next = input.value ?? "";
|
|
@@ -1467,7 +1495,7 @@
|
|
|
1467
1495
|
let autofillTimer = null;
|
|
1468
1496
|
let autofillProbe = false;
|
|
1469
1497
|
const scheduleAutofillProbe = () => {
|
|
1470
|
-
if (autofillProbe) return;
|
|
1498
|
+
if (autofillDisposed || autofillProbe) return;
|
|
1471
1499
|
autofillProbe = true;
|
|
1472
1500
|
let count = 0;
|
|
1473
1501
|
const tick = () => {
|
|
@@ -1477,12 +1505,12 @@
|
|
|
1477
1505
|
autofillProbe = false;
|
|
1478
1506
|
return;
|
|
1479
1507
|
}
|
|
1480
|
-
|
|
1508
|
+
autofillLater(tick, 120);
|
|
1481
1509
|
};
|
|
1482
|
-
|
|
1510
|
+
autofillLater(tick, 40);
|
|
1483
1511
|
};
|
|
1484
1512
|
const startAutofillWatch = () => {
|
|
1485
|
-
if (autofillTimer) return;
|
|
1513
|
+
if (autofillDisposed || autofillTimer) return;
|
|
1486
1514
|
autofillTimer = setInterval(() => {
|
|
1487
1515
|
if (document.activeElement !== input) {
|
|
1488
1516
|
clearInterval(autofillTimer);
|
|
@@ -1511,12 +1539,12 @@
|
|
|
1511
1539
|
props.onFocus?.(e);
|
|
1512
1540
|
startAutofillWatch();
|
|
1513
1541
|
scheduleAutofillProbe();
|
|
1514
|
-
|
|
1542
|
+
autofillLater(syncAutofill, 50);
|
|
1515
1543
|
});
|
|
1516
1544
|
input.addEventListener("blur", (e) => {
|
|
1517
1545
|
props.onBlur?.(e);
|
|
1518
1546
|
stopAutofillWatch();
|
|
1519
|
-
|
|
1547
|
+
autofillLater(syncAutofill, 0);
|
|
1520
1548
|
});
|
|
1521
1549
|
input.addEventListener("animationstart", (e) => {
|
|
1522
1550
|
if (e.animationName !== "cms-autofill") return;
|
|
@@ -1524,9 +1552,16 @@
|
|
|
1524
1552
|
startAutofillWatch();
|
|
1525
1553
|
scheduleAutofillProbe();
|
|
1526
1554
|
});
|
|
1527
|
-
|
|
1555
|
+
autofillLater(syncAutofill, 0);
|
|
1528
1556
|
scheduleAutofillProbe();
|
|
1529
1557
|
|
|
1558
|
+
JSswift._registerCleanup(input, () => {
|
|
1559
|
+
autofillDisposed = true;
|
|
1560
|
+
stopAutofillWatch();
|
|
1561
|
+
autofillTimeouts.forEach(clearTimeout);
|
|
1562
|
+
autofillTimeouts.clear();
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1530
1565
|
// model binding
|
|
1531
1566
|
const model = props.model;
|
|
1532
1567
|
if (model) {
|
|
@@ -1540,11 +1575,12 @@
|
|
|
1540
1575
|
lastKnownValue = next;
|
|
1541
1576
|
};
|
|
1542
1577
|
|
|
1543
|
-
|
|
1578
|
+
const onValue = (v) => {
|
|
1544
1579
|
if (syncing) return;
|
|
1545
1580
|
syncing = true;
|
|
1546
1581
|
try { updateFromRod(v); } finally { syncing = false; }
|
|
1547
|
-
}
|
|
1582
|
+
};
|
|
1583
|
+
rod.action(onValue);
|
|
1548
1584
|
|
|
1549
1585
|
// Component setup may run inside a dynamic parent render. This initial
|
|
1550
1586
|
// hydration must not make that parent subscribe to the control model.
|
|
@@ -1561,18 +1597,26 @@
|
|
|
1561
1597
|
input.addEventListener("input", onInput);
|
|
1562
1598
|
input.addEventListener("compositionend", onInput);
|
|
1563
1599
|
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1600
|
+
let disposed = false;
|
|
1601
|
+
const stop = () => {
|
|
1602
|
+
if (disposed) return;
|
|
1603
|
+
disposed = true;
|
|
1604
|
+
input.removeEventListener("input", onInput);
|
|
1605
|
+
input.removeEventListener("compositionend", onInput);
|
|
1606
|
+
const actionIndex = rod._actions?.indexOf(onValue) ?? -1;
|
|
1607
|
+
if (actionIndex >= 0) rod._actions.splice(actionIndex, 1);
|
|
1608
|
+
const hookIndex = rod._disposeHooks?.indexOf(stop) ?? -1;
|
|
1609
|
+
if (hookIndex >= 0 && !rod._disposed) rod._disposeHooks.splice(hookIndex, 1);
|
|
1610
|
+
};
|
|
1611
|
+
rod.onDispose?.(stop);
|
|
1612
|
+
JSswift._registerCleanup(input, stop);
|
|
1570
1613
|
};
|
|
1571
1614
|
if (typeof model === "object" && typeof model._bind === "function") {
|
|
1572
1615
|
bindInputRod(model);
|
|
1573
1616
|
} else if (Array.isArray(model) && typeof model[0] === "function" && typeof model[1] === "function") {
|
|
1574
1617
|
const r = JSswift.reactive.untracked(() => JSswift.rodFromSignal(model[0], model[1]));
|
|
1575
1618
|
bindInputRod(r);
|
|
1619
|
+
JSswift._registerCleanup(input, () => r.dispose());
|
|
1576
1620
|
}
|
|
1577
1621
|
}
|
|
1578
1622
|
|
|
@@ -1656,11 +1700,12 @@
|
|
|
1656
1700
|
textarea.value = next;
|
|
1657
1701
|
};
|
|
1658
1702
|
|
|
1659
|
-
|
|
1703
|
+
const onValue = (v) => {
|
|
1660
1704
|
if (syncing) return;
|
|
1661
1705
|
syncing = true;
|
|
1662
1706
|
try { updateFromRod(v); } finally { syncing = false; }
|
|
1663
|
-
}
|
|
1707
|
+
};
|
|
1708
|
+
rod.action(onValue);
|
|
1664
1709
|
|
|
1665
1710
|
JSswift.reactive.untracked(() => updateFromRod(rod.value));
|
|
1666
1711
|
|
|
@@ -1675,18 +1720,26 @@
|
|
|
1675
1720
|
textarea.addEventListener("input", onInput);
|
|
1676
1721
|
textarea.addEventListener("compositionend", onInput);
|
|
1677
1722
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1723
|
+
let disposed = false;
|
|
1724
|
+
const stop = () => {
|
|
1725
|
+
if (disposed) return;
|
|
1726
|
+
disposed = true;
|
|
1727
|
+
textarea.removeEventListener("input", onInput);
|
|
1728
|
+
textarea.removeEventListener("compositionend", onInput);
|
|
1729
|
+
const actionIndex = rod._actions?.indexOf(onValue) ?? -1;
|
|
1730
|
+
if (actionIndex >= 0) rod._actions.splice(actionIndex, 1);
|
|
1731
|
+
const hookIndex = rod._disposeHooks?.indexOf(stop) ?? -1;
|
|
1732
|
+
if (hookIndex >= 0 && !rod._disposed) rod._disposeHooks.splice(hookIndex, 1);
|
|
1733
|
+
};
|
|
1734
|
+
rod.onDispose?.(stop);
|
|
1735
|
+
JSswift._registerCleanup(textarea, stop);
|
|
1684
1736
|
};
|
|
1685
1737
|
if (typeof model === "object" && typeof model._bind === "function") {
|
|
1686
1738
|
bindTextareaRod(model);
|
|
1687
1739
|
} else if (Array.isArray(model) && typeof model[0] === "function" && typeof model[1] === "function") {
|
|
1688
1740
|
const r = JSswift.reactive.untracked(() => JSswift.rodFromSignal(model[0], model[1]));
|
|
1689
1741
|
bindTextareaRod(r);
|
|
1742
|
+
JSswift._registerCleanup(textarea, () => r.dispose());
|
|
1690
1743
|
}
|
|
1691
1744
|
}
|
|
1692
1745
|
|
|
@@ -1722,7 +1775,22 @@
|
|
|
1722
1775
|
};
|
|
1723
1776
|
|
|
1724
1777
|
|
|
1778
|
+
// Component-owned subscriptions; shared models remain caller-owned.
|
|
1779
|
+
function uiSubscriptionScope() {
|
|
1780
|
+
const stops = [];
|
|
1781
|
+
return {
|
|
1782
|
+
effect: (fn, name) => { const stop = JSswift.reactive.effect(fn, name); stops.push(stop); return stop; },
|
|
1783
|
+
action: (rod, fn) => {
|
|
1784
|
+
rod.action(fn);
|
|
1785
|
+
stops.push(() => { const i = rod._actions?.indexOf(fn) ?? -1; if (i >= 0) rod._actions.splice(i, 1); });
|
|
1786
|
+
},
|
|
1787
|
+
dispose: () => stops.splice(0).forEach(stop => stop())
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1725
1791
|
UI.Search = (props = {}) => {
|
|
1792
|
+
const scope = uiSubscriptionScope();
|
|
1793
|
+
let disposed = false;
|
|
1726
1794
|
const slots = props.slots || {};
|
|
1727
1795
|
const isBoundValue = (v) => !!v && ((typeof v === "object" && typeof v._bind === "function") || uiIsSignal(v));
|
|
1728
1796
|
const valueBinding = isBoundValue(props.model)
|
|
@@ -1835,6 +1903,7 @@
|
|
|
1835
1903
|
setActive(-1);
|
|
1836
1904
|
};
|
|
1837
1905
|
const runSearch = async (query, options = {}) => {
|
|
1906
|
+
if (disposed) return;
|
|
1838
1907
|
const q = query == null ? "" : String(query).trim();
|
|
1839
1908
|
const id = ++requestId;
|
|
1840
1909
|
const endpoint = uiUnwrap(props.endpoint ?? props.url);
|
|
@@ -1889,7 +1958,7 @@
|
|
|
1889
1958
|
} else {
|
|
1890
1959
|
raw = [];
|
|
1891
1960
|
}
|
|
1892
|
-
if (id !== requestId) return;
|
|
1961
|
+
if (disposed || id !== requestId) return;
|
|
1893
1962
|
const mapped = typeof props.mapResponse === "function" ? props.mapResponse(raw, q) : normalizeResults(raw);
|
|
1894
1963
|
const results = normalizeResults(mapped);
|
|
1895
1964
|
if (cacheEnabled) cache.set(cacheKey, results);
|
|
@@ -1898,13 +1967,13 @@
|
|
|
1898
1967
|
props.onResults?.(results, q);
|
|
1899
1968
|
} catch (error) {
|
|
1900
1969
|
if (error?.name === "AbortError") return;
|
|
1901
|
-
if (id !== requestId) return;
|
|
1970
|
+
if (disposed || id !== requestId) return;
|
|
1902
1971
|
setError(error);
|
|
1903
1972
|
setResults([]);
|
|
1904
1973
|
setActive(-1);
|
|
1905
1974
|
props.onError?.(error, q);
|
|
1906
1975
|
} finally {
|
|
1907
|
-
if (id === requestId) {
|
|
1976
|
+
if (!disposed && id === requestId) {
|
|
1908
1977
|
setLoading(false);
|
|
1909
1978
|
activeController = null;
|
|
1910
1979
|
scheduleMenuPosition();
|
|
@@ -1912,6 +1981,7 @@
|
|
|
1912
1981
|
}
|
|
1913
1982
|
};
|
|
1914
1983
|
const scheduleSearch = (query, options = {}) => {
|
|
1984
|
+
if (disposed) return;
|
|
1915
1985
|
if (searchTimer) clearTimeout(searchTimer);
|
|
1916
1986
|
const delay = options.immediate ? 0 : debounceMs();
|
|
1917
1987
|
searchTimer = setTimeout(() => {
|
|
@@ -2072,18 +2142,18 @@
|
|
|
2072
2142
|
|
|
2073
2143
|
if (valueBinding) {
|
|
2074
2144
|
if (typeof valueBinding === "object" && typeof valueBinding._bind === "function") {
|
|
2075
|
-
|
|
2145
|
+
scope.action(valueBinding, (v) => setQuery(v, { fromModel: true }));
|
|
2076
2146
|
modelSet = (v) => {
|
|
2077
2147
|
if (valueBinding.value !== v) valueBinding.value = v;
|
|
2078
2148
|
};
|
|
2079
2149
|
} else if (Array.isArray(valueBinding) && typeof valueBinding[0] === "function" && typeof valueBinding[1] === "function") {
|
|
2080
2150
|
const get = valueBinding[0];
|
|
2081
2151
|
const set = valueBinding[1];
|
|
2082
|
-
|
|
2152
|
+
scope.effect(() => { setQuery(get(), { fromModel: true }); }, "UI.Search:model");
|
|
2083
2153
|
modelSet = (v) => set(v);
|
|
2084
2154
|
}
|
|
2085
2155
|
} else if (uiIsReactive(props.value)) {
|
|
2086
|
-
|
|
2156
|
+
scope.effect(() => { setQuery(uiUnwrap(props.value), { fromModel: true }); }, "UI.Search:value");
|
|
2087
2157
|
}
|
|
2088
2158
|
|
|
2089
2159
|
input.addEventListener("input", (event) => {
|
|
@@ -2157,7 +2227,7 @@
|
|
|
2157
2227
|
};
|
|
2158
2228
|
document.addEventListener("click", onDocClick, true);
|
|
2159
2229
|
|
|
2160
|
-
|
|
2230
|
+
scope.effect(() => {
|
|
2161
2231
|
const openNow = getOpen();
|
|
2162
2232
|
root.classList.toggle("open", openNow);
|
|
2163
2233
|
input.setAttribute("aria-expanded", openNow ? "true" : "false");
|
|
@@ -2165,7 +2235,7 @@
|
|
|
2165
2235
|
else unmountMenuPortal();
|
|
2166
2236
|
}, "UI.Search:open");
|
|
2167
2237
|
|
|
2168
|
-
|
|
2238
|
+
scope.effect(() => {
|
|
2169
2239
|
const d = isDisabled();
|
|
2170
2240
|
const r = isReadonly();
|
|
2171
2241
|
root.classList.toggle("disabled", d);
|
|
@@ -2174,7 +2244,7 @@
|
|
|
2174
2244
|
input.readOnly = r;
|
|
2175
2245
|
}, "UI.Search:disabled");
|
|
2176
2246
|
|
|
2177
|
-
|
|
2247
|
+
scope.effect(() => {
|
|
2178
2248
|
const loading = getLoading();
|
|
2179
2249
|
const error = getError();
|
|
2180
2250
|
const results = getResults();
|
|
@@ -2228,7 +2298,7 @@
|
|
|
2228
2298
|
scheduleMenuPosition();
|
|
2229
2299
|
}, "UI.Search:render");
|
|
2230
2300
|
|
|
2231
|
-
|
|
2301
|
+
scope.effect(() => {
|
|
2232
2302
|
const active = getActive();
|
|
2233
2303
|
if (active === lastActive) return;
|
|
2234
2304
|
if (resultNodes[lastActive]) {
|
|
@@ -2271,10 +2341,12 @@
|
|
|
2271
2341
|
field._searchNow = () => runSearch(getQuery(), { immediate: true, open: true });
|
|
2272
2342
|
field._getResults = getResults;
|
|
2273
2343
|
field._setValue = (value) => setQuery(value, {});
|
|
2274
|
-
let disposed = false;
|
|
2275
2344
|
field._dispose = () => {
|
|
2276
2345
|
if (disposed) return;
|
|
2277
2346
|
disposed = true;
|
|
2347
|
+
scope.dispose();
|
|
2348
|
+
requestId++;
|
|
2349
|
+
cache.clear();
|
|
2278
2350
|
document.removeEventListener("click", onDocClick, true);
|
|
2279
2351
|
if (searchTimer) clearTimeout(searchTimer);
|
|
2280
2352
|
if (activeController) activeController.abort();
|
|
@@ -2295,6 +2367,8 @@
|
|
|
2295
2367
|
|
|
2296
2368
|
|
|
2297
2369
|
UI.Select = (props = {}) => {
|
|
2370
|
+
const scope = uiSubscriptionScope();
|
|
2371
|
+
let disposed = false;
|
|
2298
2372
|
const fieldProps = {
|
|
2299
2373
|
...props,
|
|
2300
2374
|
label: props.label ?? props.placeholder
|
|
@@ -2337,7 +2411,7 @@
|
|
|
2337
2411
|
if (valueBinding) {
|
|
2338
2412
|
if (typeof valueBinding === "object" && typeof valueBinding._bind === "function") {
|
|
2339
2413
|
JSswift.reactive.untracked(() => setValue(normalizeValue(valueBinding.value)));
|
|
2340
|
-
|
|
2414
|
+
scope.action(valueBinding, (v) => setValue(normalizeValue(v)));
|
|
2341
2415
|
modelSet = (v) => {
|
|
2342
2416
|
const next = normalizeValue(v);
|
|
2343
2417
|
if (valueBinding.value !== next) valueBinding.value = next;
|
|
@@ -2346,7 +2420,7 @@
|
|
|
2346
2420
|
const get = valueBinding[0];
|
|
2347
2421
|
const set = valueBinding[1];
|
|
2348
2422
|
JSswift.reactive.untracked(() => {
|
|
2349
|
-
|
|
2423
|
+
scope.effect(() => { setValue(normalizeValue(get())); }, "UI.Select:model");
|
|
2350
2424
|
});
|
|
2351
2425
|
modelSet = (v) => set(normalizeValue(v));
|
|
2352
2426
|
}
|
|
@@ -2413,6 +2487,7 @@
|
|
|
2413
2487
|
|
|
2414
2488
|
// supports async options
|
|
2415
2489
|
async function loadOptions() {
|
|
2490
|
+
if (disposed) return;
|
|
2416
2491
|
const src = props.options;
|
|
2417
2492
|
let raw;
|
|
2418
2493
|
|
|
@@ -2426,12 +2501,13 @@
|
|
|
2426
2501
|
} else {
|
|
2427
2502
|
raw = v;
|
|
2428
2503
|
}
|
|
2429
|
-
buildLists(raw);
|
|
2504
|
+
if (!disposed) buildLists(raw);
|
|
2430
2505
|
} catch (e) {
|
|
2506
|
+
if (disposed) return;
|
|
2431
2507
|
console.error("[UI.Select] options load error:", e);
|
|
2432
2508
|
buildLists([]);
|
|
2433
2509
|
} finally {
|
|
2434
|
-
setLoading(false);
|
|
2510
|
+
if (!disposed) setLoading(false);
|
|
2435
2511
|
}
|
|
2436
2512
|
}
|
|
2437
2513
|
|
|
@@ -2471,11 +2547,12 @@
|
|
|
2471
2547
|
}
|
|
2472
2548
|
|
|
2473
2549
|
async function open() {
|
|
2474
|
-
if (isDisabled()) return;
|
|
2550
|
+
if (disposed || isDisabled()) return;
|
|
2475
2551
|
setOpen(true);
|
|
2476
2552
|
|
|
2477
2553
|
// load options (sync/async) on open
|
|
2478
2554
|
await loadOptions();
|
|
2555
|
+
if (disposed) return;
|
|
2479
2556
|
|
|
2480
2557
|
// set active to current selected, otherwise first enabled
|
|
2481
2558
|
const current = getValue();
|
|
@@ -2513,7 +2590,7 @@
|
|
|
2513
2590
|
const valueNode = _.div({
|
|
2514
2591
|
class: uiClass(["cms-select-value", isMulti ? "cms-select-value-multi" : ""])
|
|
2515
2592
|
});
|
|
2516
|
-
|
|
2593
|
+
scope.effect(() => {
|
|
2517
2594
|
const flat = getFlat();
|
|
2518
2595
|
valueNode.innerHTML = "";
|
|
2519
2596
|
if (isMulti) {
|
|
@@ -2560,7 +2637,7 @@
|
|
|
2560
2637
|
}, "UI.Select:value");
|
|
2561
2638
|
|
|
2562
2639
|
const arrowWrap = _.div({ class: "cms-select-arrow" });
|
|
2563
|
-
|
|
2640
|
+
scope.effect(() => {
|
|
2564
2641
|
const arrowNode = JSswift.ui.renderSlot(slots, "arrow", { open: getOpen() }, UI.Icon("#chevron-down"));
|
|
2565
2642
|
arrowWrap.innerHTML = "";
|
|
2566
2643
|
renderSlotToArray(null, "default", {}, arrowNode).forEach(n => arrowWrap.appendChild(n));
|
|
@@ -2602,7 +2679,7 @@
|
|
|
2602
2679
|
if (filterInput) filterInput.value = getFilter() || "";
|
|
2603
2680
|
};
|
|
2604
2681
|
JSswift.reactive.untracked(renderFilterSlot);
|
|
2605
|
-
|
|
2682
|
+
scope.effect(() => {
|
|
2606
2683
|
const next = getFilter() || "";
|
|
2607
2684
|
if (filterInput && "value" in filterInput && filterInput.value !== next) {
|
|
2608
2685
|
filterInput.value = next;
|
|
@@ -2611,7 +2688,7 @@
|
|
|
2611
2688
|
|
|
2612
2689
|
if (typeof filterable === "boolean") filterable ? filterWrap.classList.remove("cms-d-none") : filterWrap.classList.add("cms-d-none");
|
|
2613
2690
|
else if (filterable?.action) {
|
|
2614
|
-
|
|
2691
|
+
scope.action(filterable, (v) => {
|
|
2615
2692
|
v ? filterWrap.classList.remove("cms-d-none") : filterWrap.classList.add("cms-d-none");
|
|
2616
2693
|
});
|
|
2617
2694
|
filterable.value ? filterWrap.classList.remove("cms-d-none") : filterWrap.classList.add("cms-d-none");;
|
|
@@ -2712,7 +2789,7 @@
|
|
|
2712
2789
|
root.appendChild(menu);
|
|
2713
2790
|
|
|
2714
2791
|
// open class + aria
|
|
2715
|
-
|
|
2792
|
+
scope.effect(() => {
|
|
2716
2793
|
const o = getOpen();
|
|
2717
2794
|
root.classList.toggle("open", o);
|
|
2718
2795
|
root.setAttribute("aria-expanded", o ? "true" : "false");
|
|
@@ -2720,7 +2797,7 @@
|
|
|
2720
2797
|
else unmountMenuPortal();
|
|
2721
2798
|
}, "UI.Select:open");
|
|
2722
2799
|
|
|
2723
|
-
|
|
2800
|
+
scope.effect(() => {
|
|
2724
2801
|
const d = isDisabled();
|
|
2725
2802
|
root.classList.toggle("disabled", d);
|
|
2726
2803
|
root.tabIndex = d ? -1 : 0;
|
|
@@ -2742,7 +2819,7 @@
|
|
|
2742
2819
|
if (active >= 0) optionNodes[active]?.scrollIntoView({ block: "nearest" });
|
|
2743
2820
|
}
|
|
2744
2821
|
|
|
2745
|
-
|
|
2822
|
+
scope.effect(() => {
|
|
2746
2823
|
const display = getList();
|
|
2747
2824
|
const flat = getFlat();
|
|
2748
2825
|
const filter = (getFilter() || "").toLowerCase().trim();
|
|
@@ -2839,7 +2916,7 @@
|
|
|
2839
2916
|
if (getOpen()) scheduleMenuPosition();
|
|
2840
2917
|
}, "UI.Select:render");
|
|
2841
2918
|
|
|
2842
|
-
|
|
2919
|
+
scope.effect(() => {
|
|
2843
2920
|
paintActive();
|
|
2844
2921
|
}, "UI.Select:paintActive");
|
|
2845
2922
|
|
|
@@ -2953,10 +3030,10 @@
|
|
|
2953
3030
|
root.addEventListener("keydown", onKeyDown);
|
|
2954
3031
|
filterWrap.addEventListener("keydown", onKeyDown);
|
|
2955
3032
|
|
|
2956
|
-
let disposed = false;
|
|
2957
3033
|
root._dispose = () => {
|
|
2958
3034
|
if (disposed) return;
|
|
2959
3035
|
disposed = true;
|
|
3036
|
+
scope.dispose();
|
|
2960
3037
|
document.removeEventListener("click", onDocClick, true);
|
|
2961
3038
|
root.removeEventListener("keydown", onKeyDown);
|
|
2962
3039
|
filterWrap.removeEventListener("keydown", onKeyDown);
|
|
@@ -2996,7 +3073,7 @@
|
|
|
2996
3073
|
});
|
|
2997
3074
|
|
|
2998
3075
|
// refresh has-value when value changes
|
|
2999
|
-
|
|
3076
|
+
scope.effect(() => { field._refresh?.(); }, "UI.Select:fieldRefresh");
|
|
3000
3077
|
|
|
3001
3078
|
uiConfigureFieldControl(field, root, props);
|
|
3002
3079
|
field._select = root;
|
|
@@ -3579,7 +3656,10 @@
|
|
|
3579
3656
|
return {
|
|
3580
3657
|
get: () => model.value,
|
|
3581
3658
|
set: (v) => { if (model.value !== v) model.value = v; },
|
|
3582
|
-
watch: (fn) => {
|
|
3659
|
+
watch: (fn) => {
|
|
3660
|
+
model.action?.(fn);
|
|
3661
|
+
return () => { const i = model._actions?.indexOf(fn) ?? -1; if (i >= 0) model._actions.splice(i, 1); };
|
|
3662
|
+
}
|
|
3583
3663
|
};
|
|
3584
3664
|
}
|
|
3585
3665
|
if (Array.isArray(model) && typeof model[0] === "function" && typeof model[1] === "function") {
|
|
@@ -3757,7 +3837,7 @@
|
|
|
3757
3837
|
if (getOpen() !== next) syncOpen(next);
|
|
3758
3838
|
controlledDrawer?.set(next);
|
|
3759
3839
|
};
|
|
3760
|
-
controlledDrawer?.watch?.(() => {
|
|
3840
|
+
const stopDrawerWatch = controlledDrawer?.watch?.(() => {
|
|
3761
3841
|
const next = !!controlledDrawer.get();
|
|
3762
3842
|
if (getOpen() !== next) syncOpen(next);
|
|
3763
3843
|
});
|
|
@@ -3772,7 +3852,7 @@
|
|
|
3772
3852
|
if (getNavOpen() !== next) syncNavOpen(next);
|
|
3773
3853
|
controlledNav?.set(next);
|
|
3774
3854
|
};
|
|
3775
|
-
controlledNav?.watch?.(() => {
|
|
3855
|
+
const stopNavWatch = controlledNav?.watch?.(() => {
|
|
3776
3856
|
const next = !!controlledNav.get();
|
|
3777
3857
|
if (getNavOpen() !== next) syncNavOpen(next);
|
|
3778
3858
|
});
|
|
@@ -3860,6 +3940,8 @@
|
|
|
3860
3940
|
JSswift.uiApplyResponsiveProps(p, props, JSswift.uiResponsiveStyleRules);
|
|
3861
3941
|
|
|
3862
3942
|
const root = _.div(p);
|
|
3943
|
+
if (stopDrawerWatch) JSswift._registerCleanup(root, stopDrawerWatch);
|
|
3944
|
+
if (stopNavWatch) JSswift._registerCleanup(root, stopNavWatch);
|
|
3863
3945
|
const tagPage = uiUnwrap(resolveLayoutProp("tagPage")) === true;
|
|
3864
3946
|
const tags = tagPage
|
|
3865
3947
|
? { header: "header", aside: "aside", nav: "nav", page: "main", footer: "footer" }
|
package/src/80-data.js
CHANGED
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
let searchInput = null;
|
|
47
|
+
let stopSearchWatch;
|
|
47
48
|
if (searchModel) {
|
|
48
|
-
searchModel.watch((value) => {
|
|
49
|
+
stopSearchWatch = searchModel.watch((value) => {
|
|
49
50
|
const next = String(value ?? "");
|
|
50
51
|
setQueryState(next);
|
|
51
52
|
if (searchInput && searchInput.value !== next) searchInput.value = next;
|
|
@@ -255,7 +256,7 @@
|
|
|
255
256
|
);
|
|
256
257
|
};
|
|
257
258
|
|
|
258
|
-
app.reactive.effect(() => {
|
|
259
|
+
const stopRender = app.reactive.effect(() => {
|
|
259
260
|
const rows = tableToArray(props.rows);
|
|
260
261
|
const query = String(getQuery() || "").trim();
|
|
261
262
|
const currentSort = getSort();
|
|
@@ -429,5 +430,8 @@
|
|
|
429
430
|
}, "UI.Table:render");
|
|
430
431
|
|
|
431
432
|
wrapProps.body = shell;
|
|
432
|
-
|
|
433
|
+
const card = UI.Card(wrapProps);
|
|
434
|
+
JSswift._registerCleanup(card, stopRender);
|
|
435
|
+
if (stopSearchWatch) JSswift._registerCleanup(card, stopSearchWatch);
|
|
436
|
+
return card;
|
|
433
437
|
};
|
package/src/css/README.md
CHANGED
|
@@ -15,3 +15,5 @@ Generated outputs:
|
|
|
15
15
|
- `pages/_jsswift-fe/css/min-ui.css`
|
|
16
16
|
- `pages/_jsswift-fe/css/ui.min.css`
|
|
17
17
|
|
|
18
|
+
|
|
19
|
+
`ui.css` remains the full compatible bundle. `ui-light.css` and `ui-light.min.css` omit breakpoint utilities but retain base tokens, component/mobile styles and unprefixed utilities generated by `cms-dev/make-responsive-css.mjs --base-only`. Add `responsive.css` after the light bundle for breakpoint props. All props remain available in JavaScript.
|