dalila 1.9.6 → 1.9.7
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/README.md +1 -1
- package/dist/cli/check.js +6 -1
- package/dist/runtime/bind.d.ts +11 -0
- package/dist/runtime/bind.js +450 -23
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ bind(document.getElementById('app')!, ctx);
|
|
|
76
76
|
- [when](./docs/core/when.md) — Conditional visibility
|
|
77
77
|
- [match](./docs/core/match.md) — Switch-style rendering
|
|
78
78
|
- [for](./docs/core/for.md) — List rendering with keyed diffing
|
|
79
|
-
- [Virtual Lists](./docs/core/virtual.md) — Fixed-height windowed rendering
|
|
79
|
+
- [Virtual Lists](./docs/core/virtual.md) — Fixed and dynamic-height windowed rendering with infinite-scroll hooks
|
|
80
80
|
|
|
81
81
|
### Data
|
|
82
82
|
|
package/dist/cli/check.js
CHANGED
|
@@ -520,7 +520,7 @@ function extractTemplateIdentifiers(html) {
|
|
|
520
520
|
i++;
|
|
521
521
|
}
|
|
522
522
|
// --- 2. Directive scanning (supports single and double quotes) ---
|
|
523
|
-
const DIRECTIVE_RE = /\b(d-each|d-virtual-each|d-virtual-height|d-virtual-item-height|d-virtual-overscan|d-if|d-when|d-match|d-portal|d-html|d-attr-[a-zA-Z][\w-]*|d-bind-[a-zA-Z][\w-]*|d-on-[a-zA-Z][\w-]*|d-form-error|d-form|d-array)\s*=\s*(['"])([\s\S]*?)\2/g;
|
|
523
|
+
const DIRECTIVE_RE = /\b(d-each|d-virtual-each|d-virtual-height|d-virtual-item-height|d-virtual-estimated-height|d-virtual-measure|d-virtual-infinite|d-virtual-overscan|d-if|d-when|d-match|d-portal|d-html|d-attr-[a-zA-Z][\w-]*|d-bind-[a-zA-Z][\w-]*|d-on-[a-zA-Z][\w-]*|d-form-error|d-form|d-array)\s*=\s*(['"])([\s\S]*?)\2/g;
|
|
524
524
|
DIRECTIVE_RE.lastIndex = 0;
|
|
525
525
|
let match;
|
|
526
526
|
while ((match = DIRECTIVE_RE.exec(html))) {
|
|
@@ -528,6 +528,8 @@ function extractTemplateIdentifiers(html) {
|
|
|
528
528
|
const value = match[3].trim();
|
|
529
529
|
if (!value)
|
|
530
530
|
continue;
|
|
531
|
+
if (directive === 'd-virtual-measure' && value.toLowerCase() === 'auto')
|
|
532
|
+
continue;
|
|
531
533
|
const roots = extractRootIdentifiers(value);
|
|
532
534
|
const loc = offsetToLineCol(match.index);
|
|
533
535
|
for (const name of roots) {
|
|
@@ -691,6 +693,9 @@ const LOOP_FORCED_CHECK_SOURCES = new Set([
|
|
|
691
693
|
'd-virtual-each',
|
|
692
694
|
'd-virtual-height',
|
|
693
695
|
'd-virtual-item-height',
|
|
696
|
+
'd-virtual-estimated-height',
|
|
697
|
+
'd-virtual-measure',
|
|
698
|
+
'd-virtual-infinite',
|
|
694
699
|
'd-virtual-overscan',
|
|
695
700
|
]);
|
|
696
701
|
function checkHtmlContent(html, filePath, validIdentifiers, diagnostics) {
|
package/dist/runtime/bind.d.ts
CHANGED
|
@@ -70,6 +70,17 @@ export interface TransitionConfig {
|
|
|
70
70
|
leave?: (el: HTMLElement) => void;
|
|
71
71
|
duration?: number;
|
|
72
72
|
}
|
|
73
|
+
export type VirtualListAlign = 'start' | 'center' | 'end';
|
|
74
|
+
export interface VirtualScrollToIndexOptions {
|
|
75
|
+
align?: VirtualListAlign;
|
|
76
|
+
behavior?: ScrollBehavior;
|
|
77
|
+
}
|
|
78
|
+
export interface VirtualListController {
|
|
79
|
+
scrollToIndex: (index: number, options?: VirtualScrollToIndexOptions) => void;
|
|
80
|
+
refresh: () => void;
|
|
81
|
+
}
|
|
82
|
+
export declare function getVirtualListController(target: Element | null): VirtualListController | null;
|
|
83
|
+
export declare function scrollToVirtualIndex(target: Element | null, index: number, options?: VirtualScrollToIndexOptions): boolean;
|
|
73
84
|
export declare function createPortalTarget(id: string): Signal<Element | null>;
|
|
74
85
|
/**
|
|
75
86
|
* Set global defaults for all `bind()` / `mount()` calls.
|
package/dist/runtime/bind.js
CHANGED
|
@@ -1498,6 +1498,47 @@ function readVirtualHeightOption(raw, ctx) {
|
|
|
1498
1498
|
}
|
|
1499
1499
|
return trimmed;
|
|
1500
1500
|
}
|
|
1501
|
+
function readVirtualMeasureOption(raw, ctx) {
|
|
1502
|
+
if (!raw)
|
|
1503
|
+
return false;
|
|
1504
|
+
const trimmed = raw.trim();
|
|
1505
|
+
if (!trimmed)
|
|
1506
|
+
return false;
|
|
1507
|
+
if (trimmed.toLowerCase() === 'auto')
|
|
1508
|
+
return true;
|
|
1509
|
+
const fromCtx = ctx[trimmed];
|
|
1510
|
+
if (fromCtx === undefined)
|
|
1511
|
+
return false;
|
|
1512
|
+
const resolved = resolve(fromCtx);
|
|
1513
|
+
if (resolved === true)
|
|
1514
|
+
return true;
|
|
1515
|
+
if (typeof resolved === 'string' && resolved.trim().toLowerCase() === 'auto')
|
|
1516
|
+
return true;
|
|
1517
|
+
return false;
|
|
1518
|
+
}
|
|
1519
|
+
function readVirtualCallbackOption(raw, ctx, label) {
|
|
1520
|
+
if (!raw)
|
|
1521
|
+
return null;
|
|
1522
|
+
const trimmed = raw.trim();
|
|
1523
|
+
if (!trimmed)
|
|
1524
|
+
return null;
|
|
1525
|
+
const fromCtx = ctx[trimmed];
|
|
1526
|
+
if (fromCtx === undefined) {
|
|
1527
|
+
warn(`${label}: "${trimmed}" not found in context`);
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
if (typeof fromCtx === 'function' && !isSignal(fromCtx)) {
|
|
1531
|
+
return fromCtx;
|
|
1532
|
+
}
|
|
1533
|
+
if (isSignal(fromCtx)) {
|
|
1534
|
+
const resolved = fromCtx();
|
|
1535
|
+
if (typeof resolved === 'function') {
|
|
1536
|
+
return resolved;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
warn(`${label}: "${trimmed}" must resolve to a function`);
|
|
1540
|
+
return null;
|
|
1541
|
+
}
|
|
1501
1542
|
function createVirtualSpacer(template, kind) {
|
|
1502
1543
|
const spacer = template.cloneNode(false);
|
|
1503
1544
|
spacer.removeAttribute('id');
|
|
@@ -1519,13 +1560,199 @@ function createVirtualSpacer(template, kind) {
|
|
|
1519
1560
|
spacer.style.listStyle = 'none';
|
|
1520
1561
|
return spacer;
|
|
1521
1562
|
}
|
|
1563
|
+
const virtualScrollRestoreCache = new Map();
|
|
1564
|
+
const VIRTUAL_SCROLL_RESTORE_CACHE_MAX_ENTRIES = 256;
|
|
1565
|
+
function getVirtualScrollRestoreValue(key) {
|
|
1566
|
+
const value = virtualScrollRestoreCache.get(key);
|
|
1567
|
+
if (value === undefined)
|
|
1568
|
+
return undefined;
|
|
1569
|
+
// Touch entry to keep LRU ordering.
|
|
1570
|
+
virtualScrollRestoreCache.delete(key);
|
|
1571
|
+
virtualScrollRestoreCache.set(key, value);
|
|
1572
|
+
return value;
|
|
1573
|
+
}
|
|
1574
|
+
function setVirtualScrollRestoreValue(key, value) {
|
|
1575
|
+
virtualScrollRestoreCache.delete(key);
|
|
1576
|
+
virtualScrollRestoreCache.set(key, value);
|
|
1577
|
+
while (virtualScrollRestoreCache.size > VIRTUAL_SCROLL_RESTORE_CACHE_MAX_ENTRIES) {
|
|
1578
|
+
const oldestKey = virtualScrollRestoreCache.keys().next().value;
|
|
1579
|
+
if (!oldestKey)
|
|
1580
|
+
break;
|
|
1581
|
+
virtualScrollRestoreCache.delete(oldestKey);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
function clampVirtual(value, min, max) {
|
|
1585
|
+
return Math.max(min, Math.min(max, value));
|
|
1586
|
+
}
|
|
1587
|
+
function getElementPositionPath(el) {
|
|
1588
|
+
const parts = [];
|
|
1589
|
+
let current = el;
|
|
1590
|
+
while (current) {
|
|
1591
|
+
const tag = current.tagName.toLowerCase();
|
|
1592
|
+
const parentEl = current.parentElement;
|
|
1593
|
+
if (!parentEl) {
|
|
1594
|
+
parts.push(tag);
|
|
1595
|
+
break;
|
|
1596
|
+
}
|
|
1597
|
+
let index = 1;
|
|
1598
|
+
let sib = current.previousElementSibling;
|
|
1599
|
+
while (sib) {
|
|
1600
|
+
index++;
|
|
1601
|
+
sib = sib.previousElementSibling;
|
|
1602
|
+
}
|
|
1603
|
+
parts.push(`${tag}:${index}`);
|
|
1604
|
+
current = parentEl;
|
|
1605
|
+
}
|
|
1606
|
+
return parts.reverse().join('>');
|
|
1607
|
+
}
|
|
1608
|
+
const virtualRestoreDocumentIds = new WeakMap();
|
|
1609
|
+
let nextVirtualRestoreDocumentId = 0;
|
|
1610
|
+
function getVirtualRestoreDocumentId(doc) {
|
|
1611
|
+
const existing = virtualRestoreDocumentIds.get(doc);
|
|
1612
|
+
if (existing !== undefined)
|
|
1613
|
+
return existing;
|
|
1614
|
+
const next = ++nextVirtualRestoreDocumentId;
|
|
1615
|
+
virtualRestoreDocumentIds.set(doc, next);
|
|
1616
|
+
return next;
|
|
1617
|
+
}
|
|
1618
|
+
function getVirtualRestoreKey(doc, templatePath, scrollContainer, bindingName, keyBinding) {
|
|
1619
|
+
const locationPath = typeof window !== 'undefined'
|
|
1620
|
+
? `${window.location.pathname}${window.location.search}`
|
|
1621
|
+
: '';
|
|
1622
|
+
const containerIdentity = scrollContainer?.id
|
|
1623
|
+
? `#${scrollContainer.id}`
|
|
1624
|
+
: (scrollContainer ? getElementPositionPath(scrollContainer) : '');
|
|
1625
|
+
const docId = getVirtualRestoreDocumentId(doc);
|
|
1626
|
+
return `${docId}|${locationPath}|${bindingName}|${keyBinding ?? ''}|${containerIdentity}|${templatePath}`;
|
|
1627
|
+
}
|
|
1628
|
+
class VirtualHeightsIndex {
|
|
1629
|
+
constructor(itemCount, estimatedHeight) {
|
|
1630
|
+
this.itemCount = 0;
|
|
1631
|
+
this.estimatedHeight = 1;
|
|
1632
|
+
this.tree = [0];
|
|
1633
|
+
this.overrides = new Map();
|
|
1634
|
+
this.reset(itemCount, estimatedHeight);
|
|
1635
|
+
}
|
|
1636
|
+
get count() {
|
|
1637
|
+
return this.itemCount;
|
|
1638
|
+
}
|
|
1639
|
+
snapshotOverrides() {
|
|
1640
|
+
return new Map(this.overrides);
|
|
1641
|
+
}
|
|
1642
|
+
reset(itemCount, estimatedHeight, seed) {
|
|
1643
|
+
this.itemCount = Number.isFinite(itemCount) ? Math.max(0, Math.floor(itemCount)) : 0;
|
|
1644
|
+
this.estimatedHeight = Number.isFinite(estimatedHeight) ? Math.max(1, estimatedHeight) : 1;
|
|
1645
|
+
this.tree = new Array(this.itemCount + 1).fill(0);
|
|
1646
|
+
this.overrides.clear();
|
|
1647
|
+
for (let i = 0; i < this.itemCount; i++) {
|
|
1648
|
+
this.addAt(i + 1, this.estimatedHeight);
|
|
1649
|
+
}
|
|
1650
|
+
if (!seed)
|
|
1651
|
+
return;
|
|
1652
|
+
for (const [index, height] of seed.entries()) {
|
|
1653
|
+
if (index < 0 || index >= this.itemCount)
|
|
1654
|
+
continue;
|
|
1655
|
+
this.set(index, height);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
set(index, height) {
|
|
1659
|
+
if (!Number.isFinite(height) || height <= 0)
|
|
1660
|
+
return false;
|
|
1661
|
+
if (index < 0 || index >= this.itemCount)
|
|
1662
|
+
return false;
|
|
1663
|
+
const next = Math.max(1, height);
|
|
1664
|
+
const current = this.get(index);
|
|
1665
|
+
if (Math.abs(next - current) < 0.5)
|
|
1666
|
+
return false;
|
|
1667
|
+
this.addAt(index + 1, next - current);
|
|
1668
|
+
if (Math.abs(next - this.estimatedHeight) < 0.5) {
|
|
1669
|
+
this.overrides.delete(index);
|
|
1670
|
+
}
|
|
1671
|
+
else {
|
|
1672
|
+
this.overrides.set(index, next);
|
|
1673
|
+
}
|
|
1674
|
+
return true;
|
|
1675
|
+
}
|
|
1676
|
+
get(index) {
|
|
1677
|
+
if (index < 0 || index >= this.itemCount)
|
|
1678
|
+
return this.estimatedHeight;
|
|
1679
|
+
return this.overrides.get(index) ?? this.estimatedHeight;
|
|
1680
|
+
}
|
|
1681
|
+
prefix(endExclusive) {
|
|
1682
|
+
if (endExclusive <= 0)
|
|
1683
|
+
return 0;
|
|
1684
|
+
const clampedEnd = Math.min(this.itemCount, Math.max(0, Math.floor(endExclusive)));
|
|
1685
|
+
let i = clampedEnd;
|
|
1686
|
+
let sum = 0;
|
|
1687
|
+
while (i > 0) {
|
|
1688
|
+
sum += this.tree[i];
|
|
1689
|
+
i -= i & -i;
|
|
1690
|
+
}
|
|
1691
|
+
return sum;
|
|
1692
|
+
}
|
|
1693
|
+
total() {
|
|
1694
|
+
return this.prefix(this.itemCount);
|
|
1695
|
+
}
|
|
1696
|
+
lowerBound(target) {
|
|
1697
|
+
if (this.itemCount === 0 || target <= 0)
|
|
1698
|
+
return 0;
|
|
1699
|
+
let idx = 0;
|
|
1700
|
+
let bit = 1;
|
|
1701
|
+
while ((bit << 1) <= this.itemCount)
|
|
1702
|
+
bit <<= 1;
|
|
1703
|
+
let sum = 0;
|
|
1704
|
+
while (bit > 0) {
|
|
1705
|
+
const next = idx + bit;
|
|
1706
|
+
if (next <= this.itemCount && sum + this.tree[next] < target) {
|
|
1707
|
+
idx = next;
|
|
1708
|
+
sum += this.tree[next];
|
|
1709
|
+
}
|
|
1710
|
+
bit >>= 1;
|
|
1711
|
+
}
|
|
1712
|
+
return Math.min(this.itemCount, idx);
|
|
1713
|
+
}
|
|
1714
|
+
indexAtOffset(offset) {
|
|
1715
|
+
if (this.itemCount === 0)
|
|
1716
|
+
return 0;
|
|
1717
|
+
if (!Number.isFinite(offset) || offset <= 0)
|
|
1718
|
+
return 0;
|
|
1719
|
+
const totalHeight = this.total();
|
|
1720
|
+
if (offset >= totalHeight)
|
|
1721
|
+
return this.itemCount - 1;
|
|
1722
|
+
const idx = this.lowerBound(offset + 0.0001);
|
|
1723
|
+
return clampVirtual(idx, 0, this.itemCount - 1);
|
|
1724
|
+
}
|
|
1725
|
+
addAt(treeIndex, delta) {
|
|
1726
|
+
let i = treeIndex;
|
|
1727
|
+
while (i <= this.itemCount) {
|
|
1728
|
+
this.tree[i] += delta;
|
|
1729
|
+
i += i & -i;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
function readVirtualListApi(target) {
|
|
1734
|
+
if (!target)
|
|
1735
|
+
return null;
|
|
1736
|
+
return target.__dalilaVirtualList ?? null;
|
|
1737
|
+
}
|
|
1738
|
+
export function getVirtualListController(target) {
|
|
1739
|
+
return readVirtualListApi(target);
|
|
1740
|
+
}
|
|
1741
|
+
export function scrollToVirtualIndex(target, index, options) {
|
|
1742
|
+
const controller = readVirtualListApi(target);
|
|
1743
|
+
if (!controller)
|
|
1744
|
+
return false;
|
|
1745
|
+
controller.scrollToIndex(index, options);
|
|
1746
|
+
return true;
|
|
1747
|
+
}
|
|
1522
1748
|
/**
|
|
1523
1749
|
* Bind all [d-virtual-each] directives within root.
|
|
1524
1750
|
*
|
|
1525
|
-
*
|
|
1526
|
-
* - Fixed item height (
|
|
1527
|
-
* -
|
|
1528
|
-
* -
|
|
1751
|
+
* Supports:
|
|
1752
|
+
* - Fixed item height (`d-virtual-item-height`)
|
|
1753
|
+
* - Dynamic item height (`d-virtual-measure="auto"`)
|
|
1754
|
+
* - Infinite scroll callback (`d-virtual-infinite`)
|
|
1755
|
+
* - Parent element as vertical scroll container
|
|
1529
1756
|
*/
|
|
1530
1757
|
function bindVirtualEach(root, ctx, cleanups) {
|
|
1531
1758
|
const elements = qsaIncludingRoot(root, '[d-virtual-each]')
|
|
@@ -1537,16 +1764,28 @@ function bindVirtualEach(root, ctx, cleanups) {
|
|
|
1537
1764
|
const itemHeightBinding = normalizeBinding(el.getAttribute('d-virtual-item-height'));
|
|
1538
1765
|
const itemHeightRaw = itemHeightBinding ?? el.getAttribute('d-virtual-item-height');
|
|
1539
1766
|
const itemHeightValue = readVirtualNumberOption(itemHeightRaw, ctx, 'd-virtual-item-height');
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1767
|
+
const fixedItemHeight = Number.isFinite(itemHeightValue) && itemHeightValue > 0
|
|
1768
|
+
? itemHeightValue
|
|
1769
|
+
: NaN;
|
|
1770
|
+
const dynamicHeight = readVirtualMeasureOption(normalizeBinding(el.getAttribute('d-virtual-measure')) ?? el.getAttribute('d-virtual-measure'), ctx);
|
|
1771
|
+
if (!dynamicHeight && (!Number.isFinite(fixedItemHeight) || fixedItemHeight <= 0)) {
|
|
1542
1772
|
warn(`d-virtual-each: invalid item height on "${bindingName}". Falling back to d-each.`);
|
|
1543
1773
|
el.setAttribute('d-each', bindingName);
|
|
1544
1774
|
el.removeAttribute('d-virtual-each');
|
|
1545
1775
|
el.removeAttribute('d-virtual-item-height');
|
|
1776
|
+
el.removeAttribute('d-virtual-estimated-height');
|
|
1777
|
+
el.removeAttribute('d-virtual-measure');
|
|
1778
|
+
el.removeAttribute('d-virtual-infinite');
|
|
1546
1779
|
el.removeAttribute('d-virtual-overscan');
|
|
1547
1780
|
el.removeAttribute('d-virtual-height');
|
|
1548
1781
|
continue;
|
|
1549
1782
|
}
|
|
1783
|
+
const estimatedHeightBinding = normalizeBinding(el.getAttribute('d-virtual-estimated-height'));
|
|
1784
|
+
const estimatedHeightRaw = estimatedHeightBinding ?? el.getAttribute('d-virtual-estimated-height');
|
|
1785
|
+
const estimatedHeightValue = readVirtualNumberOption(estimatedHeightRaw, ctx, 'd-virtual-estimated-height');
|
|
1786
|
+
const estimatedItemHeight = Number.isFinite(estimatedHeightValue) && estimatedHeightValue > 0
|
|
1787
|
+
? estimatedHeightValue
|
|
1788
|
+
: (Number.isFinite(fixedItemHeight) ? fixedItemHeight : 48);
|
|
1550
1789
|
const overscanBinding = normalizeBinding(el.getAttribute('d-virtual-overscan'));
|
|
1551
1790
|
const overscanRaw = overscanBinding ?? el.getAttribute('d-virtual-overscan');
|
|
1552
1791
|
const overscanValue = readVirtualNumberOption(overscanRaw, ctx, 'd-virtual-overscan');
|
|
@@ -1554,15 +1793,20 @@ function bindVirtualEach(root, ctx, cleanups) {
|
|
|
1554
1793
|
? Math.max(0, Math.floor(overscanValue))
|
|
1555
1794
|
: 6;
|
|
1556
1795
|
const viewportHeight = readVirtualHeightOption(normalizeBinding(el.getAttribute('d-virtual-height')) ?? el.getAttribute('d-virtual-height'), ctx);
|
|
1796
|
+
const onEndReached = readVirtualCallbackOption(normalizeBinding(el.getAttribute('d-virtual-infinite')) ?? el.getAttribute('d-virtual-infinite'), ctx, 'd-virtual-infinite');
|
|
1557
1797
|
let binding = ctx[bindingName];
|
|
1558
1798
|
if (binding === undefined) {
|
|
1559
1799
|
warn(`d-virtual-each: "${bindingName}" not found in context`);
|
|
1560
1800
|
binding = [];
|
|
1561
1801
|
}
|
|
1802
|
+
const templatePathBeforeDetach = getElementPositionPath(el);
|
|
1562
1803
|
const comment = document.createComment('d-virtual-each');
|
|
1563
1804
|
el.parentNode?.replaceChild(comment, el);
|
|
1564
1805
|
el.removeAttribute('d-virtual-each');
|
|
1565
1806
|
el.removeAttribute('d-virtual-item-height');
|
|
1807
|
+
el.removeAttribute('d-virtual-estimated-height');
|
|
1808
|
+
el.removeAttribute('d-virtual-measure');
|
|
1809
|
+
el.removeAttribute('d-virtual-infinite');
|
|
1566
1810
|
el.removeAttribute('d-virtual-overscan');
|
|
1567
1811
|
el.removeAttribute('d-virtual-height');
|
|
1568
1812
|
const keyBinding = normalizeBinding(el.getAttribute('d-key'));
|
|
@@ -1579,8 +1823,14 @@ function bindVirtualEach(root, ctx, cleanups) {
|
|
|
1579
1823
|
if (!scrollContainer.style.overflowY)
|
|
1580
1824
|
scrollContainer.style.overflowY = 'auto';
|
|
1581
1825
|
}
|
|
1826
|
+
const restoreKey = getVirtualRestoreKey(el.ownerDocument, templatePathBeforeDetach, scrollContainer, bindingName, keyBinding);
|
|
1827
|
+
const savedScrollTop = getVirtualScrollRestoreValue(restoreKey);
|
|
1828
|
+
if (scrollContainer && Number.isFinite(savedScrollTop)) {
|
|
1829
|
+
scrollContainer.scrollTop = Math.max(0, savedScrollTop);
|
|
1830
|
+
}
|
|
1582
1831
|
const clonesByKey = new Map();
|
|
1583
1832
|
const disposesByKey = new Map();
|
|
1833
|
+
const observedElements = new Set();
|
|
1584
1834
|
const metadataByKey = new Map();
|
|
1585
1835
|
const itemsByKey = new Map();
|
|
1586
1836
|
const objectKeyIds = new WeakMap();
|
|
@@ -1590,6 +1840,7 @@ function bindVirtualEach(root, ctx, cleanups) {
|
|
|
1590
1840
|
const missingKeyWarned = new Set();
|
|
1591
1841
|
let warnedNonArray = false;
|
|
1592
1842
|
let warnedViewportFallback = false;
|
|
1843
|
+
let heightsIndex = dynamicHeight ? new VirtualHeightsIndex(0, estimatedItemHeight) : null;
|
|
1593
1844
|
const getObjectKeyId = (value) => {
|
|
1594
1845
|
const existing = objectKeyIds.get(value);
|
|
1595
1846
|
if (existing !== undefined)
|
|
@@ -1644,6 +1895,7 @@ function bindVirtualEach(root, ctx, cleanups) {
|
|
|
1644
1895
|
}
|
|
1645
1896
|
return index;
|
|
1646
1897
|
};
|
|
1898
|
+
let rowResizeObserver = null;
|
|
1647
1899
|
function createClone(key, item, index, count) {
|
|
1648
1900
|
const clone = template.cloneNode(true);
|
|
1649
1901
|
const itemCtx = Object.create(ctx);
|
|
@@ -1669,6 +1921,7 @@ function bindVirtualEach(root, ctx, cleanups) {
|
|
|
1669
1921
|
itemCtx.$odd = metadata.$odd;
|
|
1670
1922
|
itemCtx.$even = metadata.$even;
|
|
1671
1923
|
clone.setAttribute('data-dalila-internal-bound', '');
|
|
1924
|
+
clone.setAttribute('data-dalila-virtual-index', String(index));
|
|
1672
1925
|
const dispose = bind(clone, itemCtx, { _skipLifecycle: true });
|
|
1673
1926
|
disposesByKey.set(key, dispose);
|
|
1674
1927
|
clonesByKey.set(key, clone);
|
|
@@ -1684,9 +1937,17 @@ function bindVirtualEach(root, ctx, cleanups) {
|
|
|
1684
1937
|
metadata.$odd.set(index % 2 !== 0);
|
|
1685
1938
|
metadata.$even.set(index % 2 === 0);
|
|
1686
1939
|
}
|
|
1940
|
+
const clone = clonesByKey.get(key);
|
|
1941
|
+
if (clone) {
|
|
1942
|
+
clone.setAttribute('data-dalila-virtual-index', String(index));
|
|
1943
|
+
}
|
|
1687
1944
|
}
|
|
1688
1945
|
function removeKey(key) {
|
|
1689
1946
|
const clone = clonesByKey.get(key);
|
|
1947
|
+
if (clone && rowResizeObserver && observedElements.has(clone)) {
|
|
1948
|
+
rowResizeObserver.unobserve(clone);
|
|
1949
|
+
observedElements.delete(clone);
|
|
1950
|
+
}
|
|
1690
1951
|
clone?.remove();
|
|
1691
1952
|
clonesByKey.delete(key);
|
|
1692
1953
|
metadataByKey.delete(key);
|
|
@@ -1698,31 +1959,106 @@ function bindVirtualEach(root, ctx, cleanups) {
|
|
|
1698
1959
|
}
|
|
1699
1960
|
}
|
|
1700
1961
|
let currentItems = [];
|
|
1962
|
+
let lastEndReachedCount = -1;
|
|
1963
|
+
let endReachedPending = false;
|
|
1964
|
+
const remapDynamicHeights = (prevItems, nextItems) => {
|
|
1965
|
+
if (!dynamicHeight || !heightsIndex)
|
|
1966
|
+
return;
|
|
1967
|
+
const heightsByKey = new Map();
|
|
1968
|
+
for (let i = 0; i < prevItems.length; i++) {
|
|
1969
|
+
const key = keyValueToString(readKeyValue(prevItems[i], i), i);
|
|
1970
|
+
if (!heightsByKey.has(key)) {
|
|
1971
|
+
heightsByKey.set(key, heightsIndex.get(i));
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
heightsIndex.reset(nextItems.length, estimatedItemHeight);
|
|
1975
|
+
for (let i = 0; i < nextItems.length; i++) {
|
|
1976
|
+
const key = keyValueToString(readKeyValue(nextItems[i], i), i);
|
|
1977
|
+
const height = heightsByKey.get(key);
|
|
1978
|
+
if (height !== undefined) {
|
|
1979
|
+
heightsIndex.set(i, height);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
const replaceItems = (nextItems) => {
|
|
1984
|
+
remapDynamicHeights(currentItems, nextItems);
|
|
1985
|
+
currentItems = nextItems;
|
|
1986
|
+
};
|
|
1987
|
+
const maybeTriggerEndReached = (visibleEnd, totalCount) => {
|
|
1988
|
+
if (!onEndReached || totalCount === 0)
|
|
1989
|
+
return;
|
|
1990
|
+
if (visibleEnd < totalCount)
|
|
1991
|
+
return;
|
|
1992
|
+
if (lastEndReachedCount === totalCount || endReachedPending)
|
|
1993
|
+
return;
|
|
1994
|
+
lastEndReachedCount = totalCount;
|
|
1995
|
+
const result = onEndReached();
|
|
1996
|
+
if (result && typeof result.then === 'function') {
|
|
1997
|
+
endReachedPending = true;
|
|
1998
|
+
Promise.resolve(result)
|
|
1999
|
+
.catch(() => { })
|
|
2000
|
+
.finally(() => {
|
|
2001
|
+
endReachedPending = false;
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
1701
2005
|
function renderVirtualList(items) {
|
|
1702
2006
|
const parent = comment.parentNode;
|
|
1703
2007
|
if (!parent)
|
|
1704
2008
|
return;
|
|
2009
|
+
if (dynamicHeight && heightsIndex && heightsIndex.count !== items.length) {
|
|
2010
|
+
heightsIndex.reset(items.length, estimatedItemHeight);
|
|
2011
|
+
}
|
|
1705
2012
|
const viewportHeightValue = scrollContainer?.clientHeight ?? 0;
|
|
1706
|
-
const effectiveViewportHeight = viewportHeightValue > 0
|
|
2013
|
+
const effectiveViewportHeight = viewportHeightValue > 0
|
|
2014
|
+
? viewportHeightValue
|
|
2015
|
+
: (dynamicHeight ? estimatedItemHeight * 10 : fixedItemHeight * 10);
|
|
1707
2016
|
const scrollTop = scrollContainer?.scrollTop ?? 0;
|
|
1708
2017
|
if (viewportHeightValue <= 0 && !warnedViewportFallback) {
|
|
1709
2018
|
warnedViewportFallback = true;
|
|
1710
2019
|
warn('d-virtual-each: scroll container has no measurable height. Using fallback viewport size.');
|
|
1711
2020
|
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
2021
|
+
let start = 0;
|
|
2022
|
+
let end = 0;
|
|
2023
|
+
let topOffset = 0;
|
|
2024
|
+
let bottomOffset = 0;
|
|
2025
|
+
let totalHeight = 0;
|
|
2026
|
+
let visibleEndForEndReached = 0;
|
|
2027
|
+
if (dynamicHeight && heightsIndex) {
|
|
2028
|
+
totalHeight = heightsIndex.total();
|
|
2029
|
+
if (items.length > 0) {
|
|
2030
|
+
const visibleStart = heightsIndex.indexAtOffset(scrollTop);
|
|
2031
|
+
const visibleEnd = clampVirtual(heightsIndex.lowerBound(scrollTop + effectiveViewportHeight) + 1, visibleStart + 1, items.length);
|
|
2032
|
+
visibleEndForEndReached = visibleEnd;
|
|
2033
|
+
start = clampVirtual(visibleStart - overscan, 0, items.length);
|
|
2034
|
+
end = clampVirtual(visibleEnd + overscan, start, items.length);
|
|
2035
|
+
topOffset = heightsIndex.prefix(start);
|
|
2036
|
+
bottomOffset = Math.max(0, totalHeight - heightsIndex.prefix(end));
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
else {
|
|
2040
|
+
const range = computeVirtualRange({
|
|
2041
|
+
itemCount: items.length,
|
|
2042
|
+
itemHeight: fixedItemHeight,
|
|
2043
|
+
scrollTop,
|
|
2044
|
+
viewportHeight: effectiveViewportHeight,
|
|
2045
|
+
overscan,
|
|
2046
|
+
});
|
|
2047
|
+
start = range.start;
|
|
2048
|
+
end = range.end;
|
|
2049
|
+
topOffset = range.topOffset;
|
|
2050
|
+
bottomOffset = range.bottomOffset;
|
|
2051
|
+
totalHeight = range.totalHeight;
|
|
2052
|
+
visibleEndForEndReached = clampVirtual(Math.ceil((scrollTop + effectiveViewportHeight) / fixedItemHeight), 0, items.length);
|
|
2053
|
+
}
|
|
2054
|
+
topSpacer.style.height = `${topOffset}px`;
|
|
2055
|
+
bottomSpacer.style.height = `${bottomOffset}px`;
|
|
2056
|
+
topSpacer.setAttribute('data-dalila-virtual-total', String(totalHeight));
|
|
1721
2057
|
const orderedClones = [];
|
|
1722
2058
|
const orderedKeys = [];
|
|
1723
2059
|
const nextKeys = new Set();
|
|
1724
2060
|
const changedKeys = new Set();
|
|
1725
|
-
for (let i =
|
|
2061
|
+
for (let i = start; i < end; i++) {
|
|
1726
2062
|
const item = items[i];
|
|
1727
2063
|
let key = keyValueToString(readKeyValue(item, i), i);
|
|
1728
2064
|
if (nextKeys.has(key)) {
|
|
@@ -1748,7 +2084,7 @@ function bindVirtualEach(root, ctx, cleanups) {
|
|
|
1748
2084
|
if (!changedKeys.has(key))
|
|
1749
2085
|
continue;
|
|
1750
2086
|
removeKey(key);
|
|
1751
|
-
orderedClones[i] = createClone(key, items[
|
|
2087
|
+
orderedClones[i] = createClone(key, items[start + i], start + i, items.length);
|
|
1752
2088
|
}
|
|
1753
2089
|
for (const key of Array.from(clonesByKey.keys())) {
|
|
1754
2090
|
if (nextKeys.has(key))
|
|
@@ -1763,6 +2099,22 @@ function bindVirtualEach(root, ctx, cleanups) {
|
|
|
1763
2099
|
}
|
|
1764
2100
|
referenceNode = clone;
|
|
1765
2101
|
}
|
|
2102
|
+
if (dynamicHeight && rowResizeObserver) {
|
|
2103
|
+
const nextObserved = new Set(orderedClones);
|
|
2104
|
+
for (const clone of Array.from(observedElements)) {
|
|
2105
|
+
if (nextObserved.has(clone))
|
|
2106
|
+
continue;
|
|
2107
|
+
rowResizeObserver.unobserve(clone);
|
|
2108
|
+
observedElements.delete(clone);
|
|
2109
|
+
}
|
|
2110
|
+
for (const clone of orderedClones) {
|
|
2111
|
+
if (observedElements.has(clone))
|
|
2112
|
+
continue;
|
|
2113
|
+
rowResizeObserver.observe(clone);
|
|
2114
|
+
observedElements.add(clone);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
maybeTriggerEndReached(visibleEndForEndReached, items.length);
|
|
1766
2118
|
}
|
|
1767
2119
|
let framePending = false;
|
|
1768
2120
|
let pendingRaf = null;
|
|
@@ -1790,28 +2142,89 @@ function bindVirtualEach(root, ctx, cleanups) {
|
|
|
1790
2142
|
const onScroll = () => scheduleRender();
|
|
1791
2143
|
const onResize = () => scheduleRender();
|
|
1792
2144
|
scrollContainer?.addEventListener('scroll', onScroll, { passive: true });
|
|
1793
|
-
|
|
2145
|
+
let containerResizeObserver = null;
|
|
2146
|
+
if (typeof ResizeObserver !== 'undefined' && scrollContainer) {
|
|
2147
|
+
containerResizeObserver = new ResizeObserver(() => scheduleRender());
|
|
2148
|
+
containerResizeObserver.observe(scrollContainer);
|
|
2149
|
+
}
|
|
2150
|
+
else if (typeof window !== 'undefined') {
|
|
1794
2151
|
window.addEventListener('resize', onResize);
|
|
1795
2152
|
}
|
|
2153
|
+
if (dynamicHeight && typeof ResizeObserver !== 'undefined' && heightsIndex) {
|
|
2154
|
+
rowResizeObserver = new ResizeObserver((entries) => {
|
|
2155
|
+
let changed = false;
|
|
2156
|
+
for (const entry of entries) {
|
|
2157
|
+
const target = entry.target;
|
|
2158
|
+
const indexRaw = target.getAttribute('data-dalila-virtual-index');
|
|
2159
|
+
if (!indexRaw)
|
|
2160
|
+
continue;
|
|
2161
|
+
const index = Number(indexRaw);
|
|
2162
|
+
if (!Number.isFinite(index))
|
|
2163
|
+
continue;
|
|
2164
|
+
const measured = entry.contentRect?.height;
|
|
2165
|
+
if (!Number.isFinite(measured) || measured <= 0)
|
|
2166
|
+
continue;
|
|
2167
|
+
changed = heightsIndex.set(index, measured) || changed;
|
|
2168
|
+
}
|
|
2169
|
+
if (changed)
|
|
2170
|
+
scheduleRender();
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
const scrollToIndex = (index, options) => {
|
|
2174
|
+
if (!scrollContainer || currentItems.length === 0)
|
|
2175
|
+
return;
|
|
2176
|
+
const safeIndex = clampVirtual(Math.floor(index), 0, currentItems.length - 1);
|
|
2177
|
+
const viewportSize = scrollContainer.clientHeight > 0
|
|
2178
|
+
? scrollContainer.clientHeight
|
|
2179
|
+
: (dynamicHeight ? estimatedItemHeight * 10 : fixedItemHeight * 10);
|
|
2180
|
+
const align = options?.align ?? 'start';
|
|
2181
|
+
let top = dynamicHeight && heightsIndex
|
|
2182
|
+
? heightsIndex.prefix(safeIndex)
|
|
2183
|
+
: safeIndex * fixedItemHeight;
|
|
2184
|
+
const itemSize = dynamicHeight && heightsIndex
|
|
2185
|
+
? heightsIndex.get(safeIndex)
|
|
2186
|
+
: fixedItemHeight;
|
|
2187
|
+
if (align === 'center') {
|
|
2188
|
+
top = top - (viewportSize / 2) + (itemSize / 2);
|
|
2189
|
+
}
|
|
2190
|
+
else if (align === 'end') {
|
|
2191
|
+
top = top - viewportSize + itemSize;
|
|
2192
|
+
}
|
|
2193
|
+
top = Math.max(0, top);
|
|
2194
|
+
if (options?.behavior && typeof scrollContainer.scrollTo === 'function') {
|
|
2195
|
+
scrollContainer.scrollTo({ top, behavior: options.behavior });
|
|
2196
|
+
}
|
|
2197
|
+
else {
|
|
2198
|
+
scrollContainer.scrollTop = top;
|
|
2199
|
+
}
|
|
2200
|
+
scheduleRender();
|
|
2201
|
+
};
|
|
2202
|
+
const virtualApi = {
|
|
2203
|
+
scrollToIndex,
|
|
2204
|
+
refresh: scheduleRender,
|
|
2205
|
+
};
|
|
2206
|
+
if (scrollContainer) {
|
|
2207
|
+
scrollContainer.__dalilaVirtualList = virtualApi;
|
|
2208
|
+
}
|
|
1796
2209
|
if (isSignal(binding)) {
|
|
1797
2210
|
bindEffect(scrollContainer ?? el, () => {
|
|
1798
2211
|
const value = binding();
|
|
1799
2212
|
if (Array.isArray(value)) {
|
|
1800
2213
|
warnedNonArray = false;
|
|
1801
|
-
|
|
2214
|
+
replaceItems(value);
|
|
1802
2215
|
}
|
|
1803
2216
|
else {
|
|
1804
2217
|
if (!warnedNonArray) {
|
|
1805
2218
|
warnedNonArray = true;
|
|
1806
2219
|
warn(`d-virtual-each: "${bindingName}" is not an array or signal-of-array`);
|
|
1807
2220
|
}
|
|
1808
|
-
|
|
2221
|
+
replaceItems([]);
|
|
1809
2222
|
}
|
|
1810
2223
|
renderVirtualList(currentItems);
|
|
1811
2224
|
});
|
|
1812
2225
|
}
|
|
1813
2226
|
else if (Array.isArray(binding)) {
|
|
1814
|
-
|
|
2227
|
+
replaceItems(binding);
|
|
1815
2228
|
renderVirtualList(currentItems);
|
|
1816
2229
|
}
|
|
1817
2230
|
else {
|
|
@@ -1819,7 +2232,10 @@ function bindVirtualEach(root, ctx, cleanups) {
|
|
|
1819
2232
|
}
|
|
1820
2233
|
cleanups.push(() => {
|
|
1821
2234
|
scrollContainer?.removeEventListener('scroll', onScroll);
|
|
1822
|
-
if (
|
|
2235
|
+
if (containerResizeObserver) {
|
|
2236
|
+
containerResizeObserver.disconnect();
|
|
2237
|
+
}
|
|
2238
|
+
else if (typeof window !== 'undefined') {
|
|
1823
2239
|
window.removeEventListener('resize', onResize);
|
|
1824
2240
|
}
|
|
1825
2241
|
if (pendingRaf != null && typeof cancelAnimationFrame === 'function') {
|
|
@@ -1829,6 +2245,17 @@ function bindVirtualEach(root, ctx, cleanups) {
|
|
|
1829
2245
|
if (pendingTimeout != null)
|
|
1830
2246
|
clearTimeout(pendingTimeout);
|
|
1831
2247
|
pendingTimeout = null;
|
|
2248
|
+
if (rowResizeObserver) {
|
|
2249
|
+
rowResizeObserver.disconnect();
|
|
2250
|
+
}
|
|
2251
|
+
observedElements.clear();
|
|
2252
|
+
if (scrollContainer) {
|
|
2253
|
+
setVirtualScrollRestoreValue(restoreKey, scrollContainer.scrollTop);
|
|
2254
|
+
const host = scrollContainer;
|
|
2255
|
+
if (host.__dalilaVirtualList === virtualApi) {
|
|
2256
|
+
delete host.__dalilaVirtualList;
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
1832
2259
|
for (const key of Array.from(clonesByKey.keys()))
|
|
1833
2260
|
removeKey(key);
|
|
1834
2261
|
topSpacer.remove();
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module dalila/runtime
|
|
8
8
|
*/
|
|
9
|
-
export { bind, autoBind, mount, configure, createPortalTarget } from './bind.js';
|
|
10
|
-
export type { BindOptions, BindContext, BindData, DisposeFunction, BindHandle, TransitionConfig } from './bind.js';
|
|
9
|
+
export { bind, autoBind, mount, configure, createPortalTarget, getVirtualListController, scrollToVirtualIndex } from './bind.js';
|
|
10
|
+
export type { BindOptions, BindContext, BindData, DisposeFunction, BindHandle, TransitionConfig, VirtualListAlign, VirtualScrollToIndexOptions, VirtualListController } from './bind.js';
|
|
11
11
|
export { fromHtml } from './fromHtml.js';
|
|
12
12
|
export type { FromHtmlOptions } from './fromHtml.js';
|
|
13
13
|
export { defineComponent } from './component.js';
|
package/dist/runtime/index.js
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module dalila/runtime
|
|
8
8
|
*/
|
|
9
|
-
export { bind, autoBind, mount, configure, createPortalTarget } from './bind.js';
|
|
9
|
+
export { bind, autoBind, mount, configure, createPortalTarget, getVirtualListController, scrollToVirtualIndex } from './bind.js';
|
|
10
10
|
export { fromHtml } from './fromHtml.js';
|
|
11
11
|
export { defineComponent } from './component.js';
|