@x-virtual/core 0.0.1
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/LICENSE +21 -0
- package/dist/xVirtualCore.js +260 -0
- package/dist/xVirtualCore.umd.cjs +1 -0
- package/index.d.ts +164 -0
- package/package.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LiFi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
class S {
|
|
2
|
+
_options = {};
|
|
3
|
+
//配置项
|
|
4
|
+
_keyToIndexObj = {};
|
|
5
|
+
//key-index对照,包含脏数据,不做删除是因为需要On遍历收益不高
|
|
6
|
+
_chunkList = [];
|
|
7
|
+
//分块数据
|
|
8
|
+
_itemSizeMap = /* @__PURE__ */ new Map();
|
|
9
|
+
//高度缓存数据,包含脏数据,不做删除是因为需要On遍历收益不高
|
|
10
|
+
_itemCount = 0;
|
|
11
|
+
//总条数
|
|
12
|
+
constructor(t = {}) {
|
|
13
|
+
const { getKey: e = null, estimatedHeight: n = null, overscan: s = 10, chunkSize: r = 100, gap: i } = t || {};
|
|
14
|
+
if (typeof e != "function") throw new Error("The parameter getKey must be a function");
|
|
15
|
+
if (typeof n != "function") throw new Error("The parameter estimatedHeight must be a function");
|
|
16
|
+
this._options = { getKey: e, estimatedHeight: n, overscan: s, chunkSize: r, gap: Number(i) || 0 };
|
|
17
|
+
}
|
|
18
|
+
_getItemHeight = (t) => this._itemSizeMap.get(t) || 0;
|
|
19
|
+
_getChunkIndex = (t) => Math.floor(t / this._options.chunkSize);
|
|
20
|
+
_getItemTop = (t) => {
|
|
21
|
+
const e = this._keyToIndexObj[t], n = this._chunkList[this._getChunkIndex(e)];
|
|
22
|
+
let s = n?.top || 0;
|
|
23
|
+
return n && (s += n.prefixSums[Math.max(e - n.start, 0)] || 0), s + ((e || 0) + 1) * this._options.gap;
|
|
24
|
+
};
|
|
25
|
+
_setItemHeight = (t, e) => this._itemSizeMap.set(t, e || 0);
|
|
26
|
+
_getItemKey = (t) => this._options?.getKey?.(t) || null;
|
|
27
|
+
_computeChunk(t) {
|
|
28
|
+
let e = 0;
|
|
29
|
+
const n = t.prefixSums;
|
|
30
|
+
for (let s = t.start; s < t.end; s++)
|
|
31
|
+
n[s - t.start] = e, e += this._getItemHeight(this._getItemKey(s));
|
|
32
|
+
t.height = e;
|
|
33
|
+
}
|
|
34
|
+
_rebuildChunkListFrom(t = 0) {
|
|
35
|
+
const e = this._itemCount;
|
|
36
|
+
if (!e) return;
|
|
37
|
+
const { chunkSize: n } = this._options, s = this._getChunkIndex(t);
|
|
38
|
+
this._chunkList = this._chunkList?.slice?.(0, s) || [];
|
|
39
|
+
const r = this._chunkList[this._chunkList.length - 1];
|
|
40
|
+
let i = (r?.top || 0) + (r?.height || 0);
|
|
41
|
+
const o = Math.ceil(e / n) - s;
|
|
42
|
+
for (let h = 0; h < o; h++) {
|
|
43
|
+
const a = (s + h) * n, c = Math.min(a + n, e), _ = { start: a, end: c, top: i, height: 0, prefixSums: new Float32Array(c - a) };
|
|
44
|
+
this._computeChunk(_), this._chunkList.push(_), i += _.height;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
_updateChunkListFrom(t = /* @__PURE__ */ new Set()) {
|
|
48
|
+
const e = this._chunkList, n = Array.from(t || []).sort((i, o) => i - o);
|
|
49
|
+
if (!n.length) return;
|
|
50
|
+
const s = n[0];
|
|
51
|
+
n?.forEach((i) => this._computeChunk(e[i]));
|
|
52
|
+
let r = (e[s]?.top || 0) + (e[s]?.height || 0);
|
|
53
|
+
for (let i = s + 1; i < e.length; i++)
|
|
54
|
+
e[i].top = r, r += e[i].height;
|
|
55
|
+
}
|
|
56
|
+
_binarySearch(t, e) {
|
|
57
|
+
if (e <= 0 || typeof t != "number" || isNaN(t)) return 0;
|
|
58
|
+
let n = 0, s = e - 1;
|
|
59
|
+
for (; n <= s; ) {
|
|
60
|
+
const r = Math.floor((n + s) / 2), i = this._getItemKey(r);
|
|
61
|
+
if (i == null || i === "") {
|
|
62
|
+
s = r - 1;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const o = this._getItemTop(i);
|
|
66
|
+
if (t >= o && t < o + this._getItemHeight(i)) return r;
|
|
67
|
+
t < o ? s = r - 1 : n = r + 1;
|
|
68
|
+
}
|
|
69
|
+
return Math.min(Math.max(0, s), e - 1);
|
|
70
|
+
}
|
|
71
|
+
getItemTop = (t) => this._getItemTop(this._getItemKey(t));
|
|
72
|
+
getTotalHeight() {
|
|
73
|
+
const t = this._getItemKey(this._itemCount - 1);
|
|
74
|
+
return this._getItemTop(t) + this._getItemHeight(t) + this._options.gap;
|
|
75
|
+
}
|
|
76
|
+
reset() {
|
|
77
|
+
this._itemSizeMap.clear(), this._keyToIndexObj = {}, this._chunkList = [], this._itemCount = 0, this._itemsPool = [];
|
|
78
|
+
}
|
|
79
|
+
setItemCount(t) {
|
|
80
|
+
if (!t || t < 0) return this.reset();
|
|
81
|
+
let e = -1;
|
|
82
|
+
const n = this._getItemKey(this._itemCount - 1), r = t > this._itemCount && this._itemCount - 1 == this._keyToIndexObj[n] ? this._itemCount : 0;
|
|
83
|
+
for (let i = r; i < t; i++) {
|
|
84
|
+
const o = this._getItemKey(i);
|
|
85
|
+
o && (e < 0 && this._keyToIndexObj[o] !== i && (e = i), this._keyToIndexObj[o] = i, this._itemSizeMap.has(o) || this._setItemHeight(o, Number(this._options.estimatedHeight?.(i) || 0)));
|
|
86
|
+
}
|
|
87
|
+
this._itemCount = t, e > -1 && this._rebuildChunkListFrom(e);
|
|
88
|
+
}
|
|
89
|
+
batchUpdateHeight(t = []) {
|
|
90
|
+
let e = 0;
|
|
91
|
+
if (!Array.isArray(t)) return e;
|
|
92
|
+
const n = /* @__PURE__ */ new Set();
|
|
93
|
+
return t.forEach((s) => {
|
|
94
|
+
const { index: r, height: i } = s || {};
|
|
95
|
+
if (typeof r != "number" || typeof i != "number" || r < 0) return;
|
|
96
|
+
const o = this._getItemKey(r);
|
|
97
|
+
!o || this._getItemHeight(o) === i || (e += i - this._getItemHeight(o), this._setItemHeight(o, i), n.add(this._getChunkIndex(r)));
|
|
98
|
+
}), n.size > 0 && this._updateChunkListFrom(n), e;
|
|
99
|
+
}
|
|
100
|
+
getVirtualItems(t, e) {
|
|
101
|
+
if (!t) return [];
|
|
102
|
+
const { overscan: n } = this._options, s = this._itemCount, r = this._binarySearch(e, s), i = n || 10;
|
|
103
|
+
let o = Math.max(r - i, 0), h = r, a = 0;
|
|
104
|
+
for (; h < s && a < t; )
|
|
105
|
+
a += this._getItemHeight(this._getItemKey(h)), h++;
|
|
106
|
+
h = Math.min(h + i, s);
|
|
107
|
+
const c = i + i, _ = h - o;
|
|
108
|
+
if (c > _) {
|
|
109
|
+
let l = c - _;
|
|
110
|
+
o == 0 ? h = Math.min(h + l, s) : h == s && (o = Math.max(o - l, 0));
|
|
111
|
+
}
|
|
112
|
+
this._itemsPool || (this._itemsPool = []);
|
|
113
|
+
const p = this._itemsPool;
|
|
114
|
+
for (let l = o; l < h; l++) {
|
|
115
|
+
const u = this._getItemKey(l);
|
|
116
|
+
if (!u) continue;
|
|
117
|
+
const m = p[l - o];
|
|
118
|
+
m ? (m.key = u, m.index = l, m.top = this._getItemTop(u)) : p.push({ key: u, index: l, top: this._getItemTop(u) });
|
|
119
|
+
}
|
|
120
|
+
return h - o ? p.slice(0, h - o) : [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function f(d) {
|
|
124
|
+
let t = null;
|
|
125
|
+
return function(...e) {
|
|
126
|
+
const n = this;
|
|
127
|
+
t && cancelAnimationFrame(t), t = requestAnimationFrame(() => d?.apply(n, e));
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
class T {
|
|
131
|
+
static containerStyle = { position: "relative", "overflow-anchor": "none" };
|
|
132
|
+
_options = {};
|
|
133
|
+
//配置项
|
|
134
|
+
_containerEl = null;
|
|
135
|
+
//滚动容器
|
|
136
|
+
_resizeObserver = null;
|
|
137
|
+
//监听元素变化
|
|
138
|
+
_listTop = 0;
|
|
139
|
+
//列表顶部
|
|
140
|
+
isInitialized = !1;
|
|
141
|
+
//是否初始化
|
|
142
|
+
_core = null;
|
|
143
|
+
_sliceData = [];
|
|
144
|
+
_preScrollTop = 0;
|
|
145
|
+
_scrollingUp = !1;
|
|
146
|
+
constructor(t = {}) {
|
|
147
|
+
const {
|
|
148
|
+
isWindowScroll: e = !1,
|
|
149
|
+
getContainerElement: n = null,
|
|
150
|
+
getKey: s = null,
|
|
151
|
+
itemSelector: r,
|
|
152
|
+
overscan: i = 10,
|
|
153
|
+
estimatedHeight: o,
|
|
154
|
+
onChange: h = null,
|
|
155
|
+
gap: a
|
|
156
|
+
} = t || {};
|
|
157
|
+
if (typeof n != "function" || typeof s != "function")
|
|
158
|
+
throw new Error("The parameters `getContainerElement` `getKey` must be a function");
|
|
159
|
+
if (!r) throw new Error("The parameters `itemSelector` cannot be null");
|
|
160
|
+
this._options = {
|
|
161
|
+
isWindowScroll: e,
|
|
162
|
+
getContainerElement: n,
|
|
163
|
+
itemSelector: r,
|
|
164
|
+
onChange: h
|
|
165
|
+
};
|
|
166
|
+
const c = new S({
|
|
167
|
+
overscan: i,
|
|
168
|
+
getKey: s,
|
|
169
|
+
estimatedHeight: o,
|
|
170
|
+
chunkSize: 100,
|
|
171
|
+
gap: a
|
|
172
|
+
}), _ = f((p) => {
|
|
173
|
+
const l = p.map((m) => {
|
|
174
|
+
const I = Number(m.target.dataset.index);
|
|
175
|
+
let g = m.borderBoxSize;
|
|
176
|
+
g = Array.isArray(g) ? g[0] : g;
|
|
177
|
+
const y = g.blockSize;
|
|
178
|
+
return { index: I, height: y };
|
|
179
|
+
});
|
|
180
|
+
let u = c.batchUpdateHeight(l);
|
|
181
|
+
Math.abs(u) > 0 && this._onHeightChange(u);
|
|
182
|
+
});
|
|
183
|
+
this._resizeObserver = new ResizeObserver(_), this._core = c, this.isInitialized = !1;
|
|
184
|
+
}
|
|
185
|
+
_isHTMLElement = (t) => t instanceof HTMLElement;
|
|
186
|
+
_getScrollDom = () => this._options.isWindowScroll ? window : this._containerEl;
|
|
187
|
+
_scrollTo = (t) => this._getScrollDom()?.scrollTo?.(0, t);
|
|
188
|
+
_createItemStyle = (t = 0) => ({
|
|
189
|
+
position: "absolute",
|
|
190
|
+
top: 0,
|
|
191
|
+
left: 0,
|
|
192
|
+
width: this._options.columnWidth || "100%",
|
|
193
|
+
transform: `translate3d(${this._options.columnLeft || 0}, ${t}px, 0)`
|
|
194
|
+
});
|
|
195
|
+
_getItemTop = (t) => this._core?.getItemTop(t);
|
|
196
|
+
_getScrollTop() {
|
|
197
|
+
let t = this._options.isWindowScroll ? window.scrollY : this._containerEl?.scrollTop;
|
|
198
|
+
return Math.max(0, t - this._listTop);
|
|
199
|
+
}
|
|
200
|
+
_onHeightChange(t) {
|
|
201
|
+
this._changeData(!1), this._scrollingUp && this._scrollTo(this._getScrollTop() + t), this._scrollingUp = !1;
|
|
202
|
+
}
|
|
203
|
+
_handleScroll = f(() => {
|
|
204
|
+
const t = this._getScrollTop();
|
|
205
|
+
this._preScrollTop !== t && (this._changeData(!0), this._scrollingUp = this._preScrollTop > t, this._preScrollTop = t);
|
|
206
|
+
});
|
|
207
|
+
_changeData(t = !0) {
|
|
208
|
+
if (t) {
|
|
209
|
+
const e = this._options.isWindowScroll ? window.innerHeight : this._containerEl?.clientHeight;
|
|
210
|
+
this._sliceData = this._core.getVirtualItems(e, this._getScrollTop());
|
|
211
|
+
}
|
|
212
|
+
this._sliceData.forEach((e) => {
|
|
213
|
+
e.top = this._getItemTop(e.index), e.style = this._createItemStyle(e.top);
|
|
214
|
+
}), this._options?.onChange?.({ hasNewData: t });
|
|
215
|
+
}
|
|
216
|
+
_getCurrentRenderedItem = () => {
|
|
217
|
+
const t = this._options.isWindowScroll ? document : this._containerEl;
|
|
218
|
+
return Array.from(t.querySelectorAll(this._options.itemSelector) || []);
|
|
219
|
+
};
|
|
220
|
+
_calculateListTop() {
|
|
221
|
+
this._options.isWindowScroll ? this._listTop = window.scrollY + (this._containerEl?.getBoundingClientRect?.()?.top || 0) : this._listTop = 0;
|
|
222
|
+
}
|
|
223
|
+
init = () => {
|
|
224
|
+
if (this.isInitialized) return;
|
|
225
|
+
const { getContainerElement: t } = this._options;
|
|
226
|
+
if (this._containerEl = t?.(), !this._isHTMLElement(this._containerEl)) throw new Error("getContainerElement must return HTMLElement");
|
|
227
|
+
this._getScrollDom()?.addEventListener("scroll", this._handleScroll), this._calculateListTop(), this.isInitialized = !0;
|
|
228
|
+
};
|
|
229
|
+
reset = () => {
|
|
230
|
+
this.isInitialized && (this._resizeObserver?.disconnect(), this.isInitialized = !1, this._listTop = 0, this._preScrollTop = 0, this._core?.reset(), this._getScrollDom()?.removeEventListener("scroll", this._handleScroll), this._scrollTo(0), this._changeData(!0));
|
|
231
|
+
};
|
|
232
|
+
setItemCount(t) {
|
|
233
|
+
if (!this.isInitialized) throw new Error("library not initialized");
|
|
234
|
+
const e = Number(t || 0);
|
|
235
|
+
this._core?.setItemCount(e), this._changeData(!0);
|
|
236
|
+
}
|
|
237
|
+
updateRenderedItemSize() {
|
|
238
|
+
this._resizeObserver?.disconnect(), this._getCurrentRenderedItem().forEach((t, e) => {
|
|
239
|
+
t.dataset.index = this._sliceData[e]?.index, this._resizeObserver?.observe(t);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
scrollToIndex(t) {
|
|
243
|
+
requestAnimationFrame(() => {
|
|
244
|
+
const e = this._getItemTop(t);
|
|
245
|
+
this._scrollTo(this._options.isWindowScroll ? this._listTop + e : e);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
getTotalHeight = () => Math.max(0, this._core?.getTotalHeight());
|
|
249
|
+
getRangeItems = () => this._sliceData || [];
|
|
250
|
+
setColumnOffsetStyle(t, e) {
|
|
251
|
+
typeof t != "number" || typeof e != "number" || (this._options.columnWidth = t + "px", this._options.columnLeft = e + "px", this._changeData(!1));
|
|
252
|
+
}
|
|
253
|
+
calculateContainerPageTop() {
|
|
254
|
+
this._calculateListTop(), this._changeData(!0);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
export {
|
|
258
|
+
T as VirtualListAdapter,
|
|
259
|
+
S as VirtualListCore
|
|
260
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(m,g){typeof exports=="object"&&typeof module<"u"?g(exports):typeof define=="function"&&define.amd?define(["exports"],g):(m=typeof globalThis<"u"?globalThis:m||self,g(m.xVirtualCore={}))})(this,(function(m){"use strict";class g{_options={};_keyToIndexObj={};_chunkList=[];_itemSizeMap=new Map;_itemCount=0;constructor(t={}){const{getKey:e=null,estimatedHeight:n=null,overscan:s=10,chunkSize:r=100,gap:i}=t||{};if(typeof e!="function")throw new Error("The parameter getKey must be a function");if(typeof n!="function")throw new Error("The parameter estimatedHeight must be a function");this._options={getKey:e,estimatedHeight:n,overscan:s,chunkSize:r,gap:Number(i)||0}}_getItemHeight=t=>this._itemSizeMap.get(t)||0;_getChunkIndex=t=>Math.floor(t/this._options.chunkSize);_getItemTop=t=>{const e=this._keyToIndexObj[t],n=this._chunkList[this._getChunkIndex(e)];let s=n?.top||0;return n&&(s+=n.prefixSums[Math.max(e-n.start,0)]||0),s+((e||0)+1)*this._options.gap};_setItemHeight=(t,e)=>this._itemSizeMap.set(t,e||0);_getItemKey=t=>this._options?.getKey?.(t)||null;_computeChunk(t){let e=0;const n=t.prefixSums;for(let s=t.start;s<t.end;s++)n[s-t.start]=e,e+=this._getItemHeight(this._getItemKey(s));t.height=e}_rebuildChunkListFrom(t=0){const e=this._itemCount;if(!e)return;const{chunkSize:n}=this._options,s=this._getChunkIndex(t);this._chunkList=this._chunkList?.slice?.(0,s)||[];const r=this._chunkList[this._chunkList.length-1];let i=(r?.top||0)+(r?.height||0);const o=Math.ceil(e/n)-s;for(let h=0;h<o;h++){const a=(s+h)*n,c=Math.min(a+n,e),_={start:a,end:c,top:i,height:0,prefixSums:new Float32Array(c-a)};this._computeChunk(_),this._chunkList.push(_),i+=_.height}}_updateChunkListFrom(t=new Set){const e=this._chunkList,n=Array.from(t||[]).sort((i,o)=>i-o);if(!n.length)return;const s=n[0];n?.forEach(i=>this._computeChunk(e[i]));let r=(e[s]?.top||0)+(e[s]?.height||0);for(let i=s+1;i<e.length;i++)e[i].top=r,r+=e[i].height}_binarySearch(t,e){if(e<=0||typeof t!="number"||isNaN(t))return 0;let n=0,s=e-1;for(;n<=s;){const r=Math.floor((n+s)/2),i=this._getItemKey(r);if(i==null||i===""){s=r-1;continue}const o=this._getItemTop(i);if(t>=o&&t<o+this._getItemHeight(i))return r;t<o?s=r-1:n=r+1}return Math.min(Math.max(0,s),e-1)}getItemTop=t=>this._getItemTop(this._getItemKey(t));getTotalHeight(){const t=this._getItemKey(this._itemCount-1);return this._getItemTop(t)+this._getItemHeight(t)+this._options.gap}reset(){this._itemSizeMap.clear(),this._keyToIndexObj={},this._chunkList=[],this._itemCount=0,this._itemsPool=[]}setItemCount(t){if(!t||t<0)return this.reset();let e=-1;const n=this._getItemKey(this._itemCount-1),r=t>this._itemCount&&this._itemCount-1==this._keyToIndexObj[n]?this._itemCount:0;for(let i=r;i<t;i++){const o=this._getItemKey(i);o&&(e<0&&this._keyToIndexObj[o]!==i&&(e=i),this._keyToIndexObj[o]=i,this._itemSizeMap.has(o)||this._setItemHeight(o,Number(this._options.estimatedHeight?.(i)||0)))}this._itemCount=t,e>-1&&this._rebuildChunkListFrom(e)}batchUpdateHeight(t=[]){let e=0;if(!Array.isArray(t))return e;const n=new Set;return t.forEach(s=>{const{index:r,height:i}=s||{};if(typeof r!="number"||typeof i!="number"||r<0)return;const o=this._getItemKey(r);!o||this._getItemHeight(o)===i||(e+=i-this._getItemHeight(o),this._setItemHeight(o,i),n.add(this._getChunkIndex(r)))}),n.size>0&&this._updateChunkListFrom(n),e}getVirtualItems(t,e){if(!t)return[];const{overscan:n}=this._options,s=this._itemCount,r=this._binarySearch(e,s),i=n||10;let o=Math.max(r-i,0),h=r,a=0;for(;h<s&&a<t;)a+=this._getItemHeight(this._getItemKey(h)),h++;h=Math.min(h+i,s);const c=i+i,_=h-o;if(c>_){let l=c-_;o==0?h=Math.min(h+l,s):h==s&&(o=Math.max(o-l,0))}this._itemsPool||(this._itemsPool=[]);const d=this._itemsPool;for(let l=o;l<h;l++){const u=this._getItemKey(l);if(!u)continue;const p=d[l-o];p?(p.key=u,p.index=l,p.top=this._getItemTop(u)):d.push({key:u,index:l,top:this._getItemTop(u)})}return h-o?d.slice(0,h-o):[]}}function y(I){let t=null;return function(...e){const n=this;t&&cancelAnimationFrame(t),t=requestAnimationFrame(()=>I?.apply(n,e))}}class S{static containerStyle={position:"relative","overflow-anchor":"none"};_options={};_containerEl=null;_resizeObserver=null;_listTop=0;isInitialized=!1;_core=null;_sliceData=[];_preScrollTop=0;_scrollingUp=!1;constructor(t={}){const{isWindowScroll:e=!1,getContainerElement:n=null,getKey:s=null,itemSelector:r,overscan:i=10,estimatedHeight:o,onChange:h=null,gap:a}=t||{};if(typeof n!="function"||typeof s!="function")throw new Error("The parameters `getContainerElement` `getKey` must be a function");if(!r)throw new Error("The parameters `itemSelector` cannot be null");this._options={isWindowScroll:e,getContainerElement:n,itemSelector:r,onChange:h};const c=new g({overscan:i,getKey:s,estimatedHeight:o,chunkSize:100,gap:a}),_=y(d=>{const l=d.map(p=>{const T=Number(p.target.dataset.index);let f=p.borderBoxSize;f=Array.isArray(f)?f[0]:f;const b=f.blockSize;return{index:T,height:b}});let u=c.batchUpdateHeight(l);Math.abs(u)>0&&this._onHeightChange(u)});this._resizeObserver=new ResizeObserver(_),this._core=c,this.isInitialized=!1}_isHTMLElement=t=>t instanceof HTMLElement;_getScrollDom=()=>this._options.isWindowScroll?window:this._containerEl;_scrollTo=t=>this._getScrollDom()?.scrollTo?.(0,t);_createItemStyle=(t=0)=>({position:"absolute",top:0,left:0,width:this._options.columnWidth||"100%",transform:`translate3d(${this._options.columnLeft||0}, ${t}px, 0)`});_getItemTop=t=>this._core?.getItemTop(t);_getScrollTop(){let t=this._options.isWindowScroll?window.scrollY:this._containerEl?.scrollTop;return Math.max(0,t-this._listTop)}_onHeightChange(t){this._changeData(!1),this._scrollingUp&&this._scrollTo(this._getScrollTop()+t),this._scrollingUp=!1}_handleScroll=y(()=>{const t=this._getScrollTop();this._preScrollTop!==t&&(this._changeData(!0),this._scrollingUp=this._preScrollTop>t,this._preScrollTop=t)});_changeData(t=!0){if(t){const e=this._options.isWindowScroll?window.innerHeight:this._containerEl?.clientHeight;this._sliceData=this._core.getVirtualItems(e,this._getScrollTop())}this._sliceData.forEach(e=>{e.top=this._getItemTop(e.index),e.style=this._createItemStyle(e.top)}),this._options?.onChange?.({hasNewData:t})}_getCurrentRenderedItem=()=>{const t=this._options.isWindowScroll?document:this._containerEl;return Array.from(t.querySelectorAll(this._options.itemSelector)||[])};_calculateListTop(){this._options.isWindowScroll?this._listTop=window.scrollY+(this._containerEl?.getBoundingClientRect?.()?.top||0):this._listTop=0}init=()=>{if(this.isInitialized)return;const{getContainerElement:t}=this._options;if(this._containerEl=t?.(),!this._isHTMLElement(this._containerEl))throw new Error("getContainerElement must return HTMLElement");this._getScrollDom()?.addEventListener("scroll",this._handleScroll),this._calculateListTop(),this.isInitialized=!0};reset=()=>{this.isInitialized&&(this._resizeObserver?.disconnect(),this.isInitialized=!1,this._listTop=0,this._preScrollTop=0,this._core?.reset(),this._getScrollDom()?.removeEventListener("scroll",this._handleScroll),this._scrollTo(0),this._changeData(!0))};setItemCount(t){if(!this.isInitialized)throw new Error("library not initialized");const e=Number(t||0);this._core?.setItemCount(e),this._changeData(!0)}updateRenderedItemSize(){this._resizeObserver?.disconnect(),this._getCurrentRenderedItem().forEach((t,e)=>{t.dataset.index=this._sliceData[e]?.index,this._resizeObserver?.observe(t)})}scrollToIndex(t){requestAnimationFrame(()=>{const e=this._getItemTop(t);this._scrollTo(this._options.isWindowScroll?this._listTop+e:e)})}getTotalHeight=()=>Math.max(0,this._core?.getTotalHeight());getRangeItems=()=>this._sliceData||[];setColumnOffsetStyle(t,e){typeof t!="number"||typeof e!="number"||(this._options.columnWidth=t+"px",this._options.columnLeft=e+"px",this._changeData(!1))}calculateContainerPageTop(){this._calculateListTop(),this._changeData(!0)}}m.VirtualListAdapter=S,m.VirtualListCore=g,Object.defineProperty(m,Symbol.toStringTag,{value:"Module"})}));
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for VirtualListCore (index.d.ts)
|
|
3
|
+
* Generated from the provided JavaScript implementation.
|
|
4
|
+
*
|
|
5
|
+
* Notes:
|
|
6
|
+
* - Keys in the original code can be strings or numbers; we type them as `string | number`.
|
|
7
|
+
* - _keyToIndexObj in the JS uses a plain object (which coerces numeric keys to strings).
|
|
8
|
+
* - If you prefer preserving numeric keys, consider using `Map<string|number, number>` in the TS implementation.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface VirtualListOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Return the key for a given item index. May return null/undefined when the item key is not available.
|
|
14
|
+
*/
|
|
15
|
+
getKey: (index: number) => string | number | null | undefined
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Return the estimated height (in px) for a given item index.
|
|
19
|
+
*/
|
|
20
|
+
estimatedHeight: (index: number) => number
|
|
21
|
+
|
|
22
|
+
/** Number of extra items to render before/after the visible window. Default: 10. */
|
|
23
|
+
overscan?: number
|
|
24
|
+
|
|
25
|
+
/** Size of each chunk used internally for prefix-sum grouping. Default: 100. */
|
|
26
|
+
chunkSize?: number
|
|
27
|
+
|
|
28
|
+
/** Gap in pixels between items. Default: 0. */
|
|
29
|
+
gap?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface Chunk {
|
|
33
|
+
start: number
|
|
34
|
+
end: number
|
|
35
|
+
top: number
|
|
36
|
+
height: number
|
|
37
|
+
prefixSums: Float32Array
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface VirtualItem {
|
|
41
|
+
key: string | number
|
|
42
|
+
index: number
|
|
43
|
+
top: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BatchHeightItem {
|
|
47
|
+
index: number
|
|
48
|
+
height: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* VirtualListCore: core calculation engine for variable-height virtual lists.
|
|
53
|
+
*
|
|
54
|
+
* Only the public API is declared here. The original implementation contains
|
|
55
|
+
* many internal helpers and caches prefixed with `_` — these are declared
|
|
56
|
+
* as private fields for completeness, but you can remove them if you prefer
|
|
57
|
+
* a minimal declaration.
|
|
58
|
+
*/
|
|
59
|
+
export declare class VirtualListCore {
|
|
60
|
+
constructor(options: VirtualListOptions)
|
|
61
|
+
|
|
62
|
+
/** Internal options passed in constructor */
|
|
63
|
+
private _options: VirtualListOptions
|
|
64
|
+
|
|
65
|
+
/** Map/object from item key -> item index. Note: JS implementation used plain object. */
|
|
66
|
+
private _keyToIndexObj: Record<string, number>
|
|
67
|
+
|
|
68
|
+
/** Chunk array used for prefix-sum calculations */
|
|
69
|
+
private _chunkList: Chunk[]
|
|
70
|
+
|
|
71
|
+
/** Cache of measured heights: key -> height (px) */
|
|
72
|
+
private _itemSizeMap: Map<string | number, number>
|
|
73
|
+
|
|
74
|
+
/** Total number of items currently tracked */
|
|
75
|
+
private _itemCount: number
|
|
76
|
+
|
|
77
|
+
/** Optional reusable item pool used by getVirtualItems */
|
|
78
|
+
private _itemsPool?: VirtualItem[]
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the top offset (px) of the item at `index`.
|
|
82
|
+
* Returns 0 when index is invalid in the original implementation.
|
|
83
|
+
*/
|
|
84
|
+
getItemTop(index: number): number
|
|
85
|
+
|
|
86
|
+
/** Get the total scrollable height (px) computed from known measurements. */
|
|
87
|
+
getTotalHeight(): number
|
|
88
|
+
|
|
89
|
+
/** Reset all caches and counters (clears measured heights, keys, chunks, etc.). */
|
|
90
|
+
reset(): void
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Update internal item count. Accepts non-negative integer. When newItemCount
|
|
94
|
+
* is falsy or negative, reset() is invoked.
|
|
95
|
+
*/
|
|
96
|
+
setItemCount(newItemCount: number): void
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Batch update measured heights. Returns the total delta in height (new - old).
|
|
100
|
+
* Invalid entries are ignored.
|
|
101
|
+
*/
|
|
102
|
+
batchUpdateHeight(arr?: BatchHeightItem[]): number
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Main API to retrieve the currently visible (and overscanned) virtual items.
|
|
106
|
+
* - viewportHeight: visible viewport height in pixels
|
|
107
|
+
* - scrollTop: current scrollTop in pixels
|
|
108
|
+
* Returns an array of VirtualItem objects { key, index, top } in ascending index order.
|
|
109
|
+
*/
|
|
110
|
+
getVirtualItems(viewportHeight: number, scrollTop: number): VirtualItem[]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function debounceRAF<T extends (...args: any[]) => any>(func: T): (...args: Parameters<T>) => void
|
|
114
|
+
|
|
115
|
+
export interface VirtualListAdapterOptions {
|
|
116
|
+
isWindowScroll?: boolean
|
|
117
|
+
/** Should return the scrolling container HTMLElement (or null when unavailable) */
|
|
118
|
+
getContainerElement: () => HTMLElement | null
|
|
119
|
+
/** Return a stable key for the given index. May return null/undefined when not available. */
|
|
120
|
+
getKey: (index: number) => string | number | null | undefined
|
|
121
|
+
/** CSS selector for item elements that will be measured/observed (required) */
|
|
122
|
+
itemSelector: string
|
|
123
|
+
overscan?: number
|
|
124
|
+
estimatedHeight?: (index: number) => number
|
|
125
|
+
onChange?: (payload: { hasNewData: boolean }) => void
|
|
126
|
+
gap?: number
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Minimal public surface of the adapter. Internal helpers prefixed with `_` are intentionally omitted from the public API here. */
|
|
130
|
+
export declare class VirtualListAdapter {
|
|
131
|
+
static containerStyle: Record<string, string>
|
|
132
|
+
|
|
133
|
+
constructor(options?: VirtualListAdapterOptions)
|
|
134
|
+
|
|
135
|
+
/** Whether adapter has been initialized via `init()` */
|
|
136
|
+
isInitialized: boolean
|
|
137
|
+
|
|
138
|
+
/** Initialize the adapter and attach scroll listeners. Throws if `getContainerElement` does not return an HTMLElement. */
|
|
139
|
+
init(): void
|
|
140
|
+
|
|
141
|
+
/** Reset internal state, disconnect observers and remove listeners. */
|
|
142
|
+
reset(): void
|
|
143
|
+
|
|
144
|
+
/** Provide the current number of items. Will call through to the core and refresh rendered slice. */
|
|
145
|
+
setItemCount(newValLength: number): void
|
|
146
|
+
|
|
147
|
+
/** Observe currently rendered DOM nodes to measure their sizes. */
|
|
148
|
+
updateRenderedItemSize(): void
|
|
149
|
+
|
|
150
|
+
/** Scroll to a specific item index (animated via RAF). */
|
|
151
|
+
scrollToIndex(index: number): void
|
|
152
|
+
|
|
153
|
+
/** Return total content height as computed by the core. */
|
|
154
|
+
getTotalHeight(): number
|
|
155
|
+
|
|
156
|
+
/** Return the currently rendered virtual items array. */
|
|
157
|
+
getRangeItems(): import('./index.d.ts').VirtualItem[]
|
|
158
|
+
|
|
159
|
+
/** Set column width/left offset used to produce item styles. */
|
|
160
|
+
setColumnOffsetStyle(width: number, left: number): void
|
|
161
|
+
|
|
162
|
+
/** Recalculate top offset (useful when container position changes) and refresh slice. */
|
|
163
|
+
calculateContainerPageTop(): void
|
|
164
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@x-virtual/core",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "A lightweight, cross-platform virtual layout library supporting dynamic-height lists and grid layouts with smooth scrolling and efficient rendering.",
|
|
6
|
+
"homepage": "https://github.com/LIFICN/x-virtual-layout",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/LIFICN/x-virtual-layout.git",
|
|
10
|
+
"directory": "packages/core"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"index.d.ts"
|
|
16
|
+
],
|
|
17
|
+
"main": "./dist/xVirtualCore.umd.cjs",
|
|
18
|
+
"module": "./dist/xVirtualCore.js",
|
|
19
|
+
"types": "./index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./index.d.ts",
|
|
23
|
+
"import": "./dist/xVirtualCore.js",
|
|
24
|
+
"require": "./dist/xVirtualCore.umd.cjs"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"vite": "^7.3.1"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"dev": "vite build --watch",
|
|
32
|
+
"build": "vite build"
|
|
33
|
+
}
|
|
34
|
+
}
|