[codex] try vue virtual scroller for messages (#1103)
* try vue virtual scroller for messages * hide inactive virtual scroller rows
This commit is contained in:
Generated
+12
-112
@@ -16,7 +16,8 @@
|
||||
"node-edge-tts": "^1.2.10",
|
||||
"node-pty": "^1.1.0",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3"
|
||||
"socket.io-client": "^4.8.3",
|
||||
"vue-virtual-scroller": "^3.0.4"
|
||||
},
|
||||
"bin": {
|
||||
"hermes-web-ui": "bin/hermes-web-ui.mjs"
|
||||
@@ -157,7 +158,6 @@
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -167,7 +167,6 @@
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -177,7 +176,6 @@
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.7"
|
||||
@@ -193,7 +191,6 @@
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
@@ -1401,7 +1398,6 @@
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
@@ -1659,9 +1655,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1683,9 +1676,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1707,9 +1697,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1731,9 +1718,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1755,9 +1739,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1779,9 +1760,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2012,9 +1990,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2032,9 +2007,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2052,9 +2024,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2072,9 +2041,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2092,9 +2058,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2112,9 +2075,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2293,9 +2253,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2310,9 +2267,6 @@
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2327,9 +2281,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2344,9 +2295,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2361,9 +2309,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2378,9 +2323,6 @@
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2395,9 +2337,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2412,9 +2351,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2429,9 +2365,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2446,9 +2379,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2463,9 +2393,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2480,9 +2407,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2497,9 +2421,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3562,7 +3483,6 @@
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz",
|
||||
"integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.3",
|
||||
@@ -3576,7 +3496,6 @@
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz",
|
||||
"integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.35",
|
||||
@@ -3587,7 +3506,6 @@
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz",
|
||||
"integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.3",
|
||||
@@ -3605,7 +3523,6 @@
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz",
|
||||
"integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.35",
|
||||
@@ -3681,7 +3598,6 @@
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz",
|
||||
"integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.35"
|
||||
@@ -3691,7 +3607,6 @@
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz",
|
||||
"integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.35",
|
||||
@@ -3702,7 +3617,6 @@
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz",
|
||||
"integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.35",
|
||||
@@ -3715,7 +3629,6 @@
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz",
|
||||
"integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.35",
|
||||
@@ -3729,7 +3642,6 @@
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz",
|
||||
"integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/test-utils": {
|
||||
@@ -4687,7 +4599,6 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cytoscape": {
|
||||
@@ -5613,7 +5524,6 @@
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
@@ -5751,7 +5661,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
@@ -7265,9 +7174,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7289,9 +7195,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7313,9 +7216,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7337,9 +7237,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7470,7 +7367,6 @@
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
@@ -7879,7 +7775,6 @@
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -8343,7 +8238,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
@@ -8525,7 +8419,6 @@
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -9659,7 +9552,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -10363,7 +10255,7 @@
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -10957,7 +10849,6 @@
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz",
|
||||
"integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.35",
|
||||
@@ -11051,6 +10942,15 @@
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-virtual-scroller": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vue-virtual-scroller/-/vue-virtual-scroller-3.0.4.tgz",
|
||||
"integrity": "sha512-3qh3c9VUVysuXynaa4fVZ3ncx3VgD7EPRiQcj+jUVZl5u/TTkD3c27XvSEu3JGJfsJt/vVTVziZ3djiiHtW4cQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vueuc": {
|
||||
"version": "0.4.65",
|
||||
"resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.65.tgz",
|
||||
|
||||
+3
-2
@@ -63,7 +63,8 @@
|
||||
"node-edge-tts": "^1.2.10",
|
||||
"node-pty": "^1.1.0",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3"
|
||||
"socket.io-client": "^4.8.3",
|
||||
"vue-virtual-scroller": "^3.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@koa/bodyparser": "^5.0.0",
|
||||
@@ -123,4 +124,4 @@
|
||||
"vue-tsc": "^3.2.8",
|
||||
"ws": "^8.20.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type ComponentPublicInstance } from "vue";
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import {
|
||||
DynamicScroller,
|
||||
DynamicScrollerItem,
|
||||
type DynamicScrollerExposed,
|
||||
type ScrollToOptions,
|
||||
} from "vue-virtual-scroller";
|
||||
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
|
||||
|
||||
type VirtualItem = {
|
||||
id: string | number;
|
||||
}
|
||||
|
||||
type AnchorAlign = "start" | "center";
|
||||
type AnchorTarget = {
|
||||
token: number;
|
||||
index: number;
|
||||
messageId: string;
|
||||
anchorId: string;
|
||||
align: AnchorAlign;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
messages: VirtualItem[];
|
||||
estimatedItemHeight?: number;
|
||||
@@ -32,127 +48,29 @@ defineSlots<{
|
||||
after?: () => any;
|
||||
}>();
|
||||
|
||||
const scrollerRef = ref<HTMLElement | null>(null);
|
||||
const hostRef = ref<HTMLElement | null>(null);
|
||||
const scrollerRef = ref<DynamicScrollerExposed<VirtualItem> | null>(null);
|
||||
const scrollTop = ref(0);
|
||||
const viewportHeight = ref(0);
|
||||
const heightVersion = ref(0);
|
||||
const measuredHeights = new Map<string, number>();
|
||||
const observedElements = new Map<string, HTMLElement>();
|
||||
const observers = new Map<string, ResizeObserver>();
|
||||
let keepBottomUntil = 0;
|
||||
let bottomFrame: number | null = null;
|
||||
let anchorFrame: number | null = null;
|
||||
let anchorToken = 0;
|
||||
let activeAnchorTarget: AnchorTarget | null = null;
|
||||
|
||||
const messageKeys = computed(() => props.messages.map(messageKey));
|
||||
const bufferPx = computed(() => Math.max(props.estimatedItemHeight, props.estimatedItemHeight * props.overscan));
|
||||
|
||||
function messageKey(message: VirtualItem): string {
|
||||
return String(message.id);
|
||||
}
|
||||
|
||||
function itemHeight(key: string): number {
|
||||
return measuredHeights.get(key) || props.estimatedItemHeight;
|
||||
}
|
||||
|
||||
function measuredRowHeight(el: HTMLElement): number {
|
||||
return Math.ceil(el.getBoundingClientRect().height || props.estimatedItemHeight);
|
||||
}
|
||||
|
||||
function setMeasuredHeight(key: string, height: number) {
|
||||
const oldHeight = itemHeight(key);
|
||||
if (oldHeight === height) return;
|
||||
|
||||
const el = scrollerRef.value;
|
||||
const shouldKeepBottom = !!el && (Date.now() < keepBottomUntil || isNearBottom(64));
|
||||
const index = messageKeys.value.indexOf(key);
|
||||
if (index >= 0 && !shouldKeepBottom) {
|
||||
const rowTop = layout.value.offsets[index] || 0;
|
||||
const delta = height - oldHeight;
|
||||
if (el && rowTop < scrollTop.value && delta !== 0) {
|
||||
el.scrollTop = Math.max(0, el.scrollTop + delta);
|
||||
syncViewport();
|
||||
}
|
||||
}
|
||||
|
||||
measuredHeights.set(key, height);
|
||||
heightVersion.value += 1;
|
||||
if (shouldKeepBottom) scheduleScrollToBottom(2);
|
||||
}
|
||||
|
||||
const layout = computed(() => {
|
||||
heightVersion.value;
|
||||
const offsets: number[] = [];
|
||||
let total = 0;
|
||||
for (const key of messageKeys.value) {
|
||||
offsets.push(total);
|
||||
total += itemHeight(key);
|
||||
}
|
||||
return { offsets, total };
|
||||
});
|
||||
|
||||
const visibleRange = computed(() => {
|
||||
const count = props.messages.length;
|
||||
if (count === 0) return { start: 0, end: -1 };
|
||||
|
||||
const overscanPx = props.estimatedItemHeight * props.overscan;
|
||||
const startPx = Math.max(0, scrollTop.value - overscanPx);
|
||||
const endPx = scrollTop.value + viewportHeight.value + overscanPx;
|
||||
const { offsets } = layout.value;
|
||||
|
||||
let start = 0;
|
||||
let low = 0;
|
||||
let high = count - 1;
|
||||
while (low <= high) {
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
const bottom = offsets[mid] + itemHeight(messageKeys.value[mid]);
|
||||
if (bottom < startPx) low = mid + 1;
|
||||
else {
|
||||
start = mid;
|
||||
high = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
let end = start;
|
||||
while (end < count - 1 && offsets[end] < endPx) end += 1;
|
||||
return { start, end };
|
||||
});
|
||||
|
||||
const visibleMessages = computed(() => {
|
||||
const { start, end } = visibleRange.value;
|
||||
return end >= start ? props.messages.slice(start, end + 1) : [];
|
||||
});
|
||||
|
||||
const topSpacerHeight = computed(() => layout.value.offsets[visibleRange.value.start] || 0);
|
||||
const bottomSpacerHeight = computed(() => {
|
||||
const { end } = visibleRange.value;
|
||||
if (end < 0) return 0;
|
||||
const nextOffset = end + 1 < props.messages.length
|
||||
? layout.value.offsets[end + 1]
|
||||
: layout.value.total;
|
||||
return Math.max(0, layout.value.total - nextOffset);
|
||||
});
|
||||
|
||||
function setItemRef(key: string, el: Element | ComponentPublicInstance | null) {
|
||||
const existing = observedElements.get(key);
|
||||
if (existing === el) return;
|
||||
|
||||
observers.get(key)?.disconnect();
|
||||
observers.delete(key);
|
||||
observedElements.delete(key);
|
||||
|
||||
if (!(el instanceof HTMLElement)) return;
|
||||
|
||||
observedElements.set(key, el);
|
||||
if (typeof ResizeObserver === "undefined") {
|
||||
setMeasuredHeight(key, measuredRowHeight(el));
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(() => setMeasuredHeight(key, measuredRowHeight(el)));
|
||||
observer.observe(el);
|
||||
observers.set(key, observer);
|
||||
function getScrollerElement(): HTMLElement | null {
|
||||
return hostRef.value?.querySelector<HTMLElement>(".virtual-message-list") ?? null;
|
||||
}
|
||||
|
||||
function syncViewport() {
|
||||
const el = scrollerRef.value;
|
||||
const el = getScrollerElement();
|
||||
if (!el) return;
|
||||
scrollTop.value = el.scrollTop;
|
||||
viewportHeight.value = el.clientHeight;
|
||||
@@ -164,8 +82,14 @@ function handleScroll() {
|
||||
if (scrollTop.value <= props.topThreshold) emit("topReach");
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
syncViewport();
|
||||
if (Date.now() < keepBottomUntil || isNearBottom(64)) scheduleScrollToBottom(2);
|
||||
if (activeAnchorTarget) scheduleAnchorAlignment(activeAnchorTarget.token, 4);
|
||||
}
|
||||
|
||||
function isNearBottom(threshold = 200): boolean {
|
||||
const el = scrollerRef.value;
|
||||
const el = getScrollerElement();
|
||||
if (!el) return true;
|
||||
return el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
|
||||
}
|
||||
@@ -178,9 +102,11 @@ function scrollToBottom() {
|
||||
}
|
||||
|
||||
function setScrollToBottomNow() {
|
||||
const el = scrollerRef.value;
|
||||
if (!el) return;
|
||||
el.scrollTop = Math.max(0, el.scrollHeight - el.clientHeight);
|
||||
const el = getScrollerElement();
|
||||
scrollerRef.value?.scrollToBottom();
|
||||
if (el) {
|
||||
el.scrollTop = Math.max(0, el.scrollHeight - el.clientHeight);
|
||||
}
|
||||
syncViewport();
|
||||
}
|
||||
|
||||
@@ -199,18 +125,97 @@ function scheduleScrollToBottom(frames = 1) {
|
||||
bottomFrame = requestAnimationFrame(() => step(frames));
|
||||
}
|
||||
|
||||
function findTargetElement(messageId: string, anchorId: string): HTMLElement | null {
|
||||
const el = getScrollerElement();
|
||||
if (!el) return null;
|
||||
|
||||
const anchor = document.getElementById(anchorId);
|
||||
if (anchor instanceof HTMLElement && el.contains(anchor)) return anchor;
|
||||
|
||||
const message = document.getElementById(`message-${messageId}`);
|
||||
if (message instanceof HTMLElement && el.contains(message)) return message;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function alignElement(targetEl: HTMLElement, align: AnchorAlign) {
|
||||
const el = getScrollerElement();
|
||||
if (!el) return;
|
||||
|
||||
const scrollerRect = el.getBoundingClientRect();
|
||||
const targetRect = targetEl.getBoundingClientRect();
|
||||
const delta = align === "center"
|
||||
? targetRect.top + targetRect.height / 2 - (scrollerRect.top + scrollerRect.height / 2)
|
||||
: targetRect.top - scrollerRect.top - 24;
|
||||
|
||||
if (Math.abs(delta) > 1) {
|
||||
el.scrollTop = Math.max(0, el.scrollTop + delta);
|
||||
}
|
||||
syncViewport();
|
||||
}
|
||||
|
||||
function scrollToItem(index: number, options?: ScrollToOptions) {
|
||||
scrollerRef.value?.scrollToItem(index, options);
|
||||
syncViewport();
|
||||
}
|
||||
|
||||
function scheduleAnchorAlignment(token: number, frames = 1) {
|
||||
if (anchorFrame != null) cancelAnimationFrame(anchorFrame);
|
||||
|
||||
const step = (remaining: number) => {
|
||||
const target = activeAnchorTarget;
|
||||
if (!target || target.token !== token) {
|
||||
anchorFrame = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const targetEl = findTargetElement(target.messageId, target.anchorId);
|
||||
if (targetEl) {
|
||||
alignElement(targetEl, target.align);
|
||||
} else {
|
||||
scrollToItem(target.index, {
|
||||
align: target.align,
|
||||
offset: target.align === "start" ? -24 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (remaining <= 1) {
|
||||
anchorFrame = null;
|
||||
activeAnchorTarget = null;
|
||||
return;
|
||||
}
|
||||
anchorFrame = requestAnimationFrame(() => step(remaining - 1));
|
||||
};
|
||||
|
||||
anchorFrame = requestAnimationFrame(() => step(frames));
|
||||
}
|
||||
|
||||
function cancelAnchorAlignment() {
|
||||
anchorToken += 1;
|
||||
activeAnchorTarget = null;
|
||||
if (anchorFrame != null) {
|
||||
cancelAnimationFrame(anchorFrame);
|
||||
anchorFrame = null;
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToMessage(messageId: string) {
|
||||
const index = props.messages.findIndex(message => String(message.id) === messageId);
|
||||
if (index < 0) return;
|
||||
|
||||
cancelAnchorAlignment();
|
||||
const token = anchorToken;
|
||||
activeAnchorTarget = {
|
||||
token,
|
||||
index,
|
||||
messageId,
|
||||
anchorId: `message-${messageId}`,
|
||||
align: "center",
|
||||
};
|
||||
|
||||
nextTick(() => {
|
||||
const el = scrollerRef.value;
|
||||
if (!el) return;
|
||||
el.scrollTop = Math.max(0, (layout.value.offsets[index] || 0) - el.clientHeight / 2);
|
||||
syncViewport();
|
||||
nextTick(() => {
|
||||
document.getElementById(`message-${messageId}`)?.scrollIntoView({ block: "center" });
|
||||
});
|
||||
scrollToItem(index, { align: "center" });
|
||||
scheduleAnchorAlignment(token, 8);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,21 +223,24 @@ function scrollToAnchor(messageId: string, anchorId: string) {
|
||||
const index = props.messages.findIndex(message => String(message.id) === messageId);
|
||||
if (index < 0) return;
|
||||
|
||||
cancelAnchorAlignment();
|
||||
const token = anchorToken;
|
||||
activeAnchorTarget = {
|
||||
token,
|
||||
index,
|
||||
messageId,
|
||||
anchorId,
|
||||
align: "start",
|
||||
};
|
||||
|
||||
nextTick(() => {
|
||||
const el = scrollerRef.value;
|
||||
if (!el) return;
|
||||
el.scrollTop = Math.max(0, (layout.value.offsets[index] || 0) - 24);
|
||||
syncViewport();
|
||||
nextTick(() => {
|
||||
const target = document.getElementById(anchorId) || document.getElementById(`message-${messageId}`);
|
||||
target?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
syncViewport();
|
||||
});
|
||||
scrollToItem(index, { align: "start", offset: -24 });
|
||||
scheduleAnchorAlignment(token, 10);
|
||||
});
|
||||
}
|
||||
|
||||
function captureScrollPosition() {
|
||||
const el = scrollerRef.value;
|
||||
const el = getScrollerElement();
|
||||
if (!el) return null;
|
||||
return {
|
||||
scrollTop: el.scrollTop,
|
||||
@@ -243,9 +251,11 @@ function captureScrollPosition() {
|
||||
function restoreScrollPosition(snapshot: { scrollTop: number; scrollHeight: number } | null) {
|
||||
if (!snapshot) return;
|
||||
nextTick(() => {
|
||||
const el = scrollerRef.value;
|
||||
const el = getScrollerElement();
|
||||
if (!el) return;
|
||||
el.scrollTop = Math.max(0, el.scrollHeight - snapshot.scrollHeight + snapshot.scrollTop);
|
||||
const nextScrollTop = Math.max(0, el.scrollHeight - snapshot.scrollHeight + snapshot.scrollTop);
|
||||
scrollerRef.value?.scrollToPosition(nextScrollTop);
|
||||
el.scrollTop = nextScrollTop;
|
||||
syncViewport();
|
||||
});
|
||||
}
|
||||
@@ -253,36 +263,24 @@ function restoreScrollPosition(snapshot: { scrollTop: number; scrollHeight: numb
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
syncViewport();
|
||||
if (scrollerRef.value && typeof ResizeObserver !== "undefined") {
|
||||
resizeObserver = new ResizeObserver(syncViewport);
|
||||
resizeObserver.observe(scrollerRef.value);
|
||||
}
|
||||
nextTick(() => {
|
||||
syncViewport();
|
||||
const el = getScrollerElement();
|
||||
if (el && typeof ResizeObserver !== "undefined") {
|
||||
resizeObserver = new ResizeObserver(handleResize);
|
||||
resizeObserver.observe(el);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (bottomFrame != null) cancelAnimationFrame(bottomFrame);
|
||||
if (anchorFrame != null) cancelAnimationFrame(anchorFrame);
|
||||
resizeObserver?.disconnect();
|
||||
for (const observer of observers.values()) observer.disconnect();
|
||||
observers.clear();
|
||||
observedElements.clear();
|
||||
});
|
||||
|
||||
watch(messageKeys, keys => {
|
||||
const activeKeys = new Set(keys);
|
||||
for (const key of [...observedElements.keys()]) {
|
||||
if (activeKeys.has(key)) continue;
|
||||
observers.get(key)?.disconnect();
|
||||
observers.delete(key);
|
||||
observedElements.delete(key);
|
||||
}
|
||||
let droppedHeights = false;
|
||||
for (const key of [...measuredHeights.keys()]) {
|
||||
if (activeKeys.has(key)) continue;
|
||||
measuredHeights.delete(key);
|
||||
droppedHeights = true;
|
||||
}
|
||||
if (droppedHeights) heightVersion.value += 1;
|
||||
watch(messageKeys, () => {
|
||||
cancelAnchorAlignment();
|
||||
nextTick(syncViewport);
|
||||
});
|
||||
|
||||
@@ -298,51 +296,69 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="scrollerRef"
|
||||
class="virtual-message-list"
|
||||
ref="hostRef"
|
||||
class="virtual-message-list-host"
|
||||
:style="{ '--virtual-row-gap': `${rowGap}px`, '--virtual-list-padding': padding }"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<slot v-if="messages.length === 0" name="empty" />
|
||||
<template v-else>
|
||||
<slot name="before" />
|
||||
<div class="virtual-spacer" :style="{ height: `${topSpacerHeight}px` }" />
|
||||
<div
|
||||
v-for="msg in visibleMessages"
|
||||
:key="msg.id"
|
||||
:ref="(el) => setItemRef(messageKey(msg), el)"
|
||||
class="virtual-row"
|
||||
>
|
||||
<slot name="item" :message="msg" />
|
||||
</div>
|
||||
<div class="virtual-spacer" :style="{ height: `${bottomSpacerHeight}px` }" />
|
||||
</template>
|
||||
<slot name="after" />
|
||||
<DynamicScroller
|
||||
ref="scrollerRef"
|
||||
class="virtual-message-list"
|
||||
:items="messages"
|
||||
key-field="id"
|
||||
:min-item-size="estimatedItemHeight"
|
||||
:buffer="bufferPx"
|
||||
:flow-mode="true"
|
||||
:prerender="overscan"
|
||||
@scroll.passive="handleScroll"
|
||||
@resize="handleResize"
|
||||
@visible="syncViewport"
|
||||
>
|
||||
<template #empty>
|
||||
<slot name="empty" />
|
||||
</template>
|
||||
<template #before>
|
||||
<slot v-if="messages.length > 0" name="before" />
|
||||
</template>
|
||||
<template #default="{ item, index, active }">
|
||||
<DynamicScrollerItem
|
||||
:item="item"
|
||||
:index="index"
|
||||
:active="active"
|
||||
class="virtual-row"
|
||||
>
|
||||
<slot v-if="active" name="item" :message="item" />
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
<template #after>
|
||||
<slot v-if="messages.length > 0" name="after" />
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@use "@/styles/variables" as *;
|
||||
|
||||
.virtual-message-list-host {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.virtual-message-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
padding: var(--virtual-list-padding);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
background-color: $bg-card;
|
||||
position: relative;
|
||||
|
||||
.dark & {
|
||||
background-color: #333333;
|
||||
}
|
||||
}
|
||||
|
||||
.virtual-spacer {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.virtual-row {
|
||||
box-sizing: border-box;
|
||||
padding-bottom: var(--virtual-row-gap);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user