Skip to content

Instantly share code, notes, and snippets.

@derpdead
Last active September 7, 2023 16:00
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save derpdead/f35a2ec176db3f19e0e67ae16d05f41a to your computer and use it in GitHub Desktop.
Save derpdead/f35a2ec176db3f19e0e67ae16d05f41a to your computer and use it in GitHub Desktop.
<template>
<div
class="virtual-scroll"
ref="root"
:style="rootStyle"
@scroll="onScroll">
<div
class="virtual-scroll__viewport"
:style="viewportStyle">
<div
class="virtual-scroll__spacer"
:style="spacerStyle">
<slot :visible-items="visibleItems" />
</div>
</div>
</div>
</template>
<script>
export default {
name: 'VirtualScroll',
props: {
items: {
type: Array,
default: () => [],
},
rootHeight: {
type: Number,
default: 100,
},
rowHeight: {
type: Number,
default: 30,
},
},
data() {
return {
scrollTop: 0,
renderAhead: 2, // It might be passed as prop
};
},
computed: {
childPositions() {
const results = [
0,
];
for (let i = 1; i < this.rowCount; i++) {
// getChildHeight might be passed as prop and return fixed height for different type of row
// results.push(results[i - 1] + getChildHeight(i - 1));
results.push(results[i - 1] + this.rowHeight);
}
return results;
},
totalHeight() {
return this.rowCount
? this.childPositions[this.rowCount - 1] + this.rowHeight
// ? this.childPositions[this.rowCount - 1] + getChildHeight(rowCount - 1)
: 0;
},
firstVisibleNode() {
return this.findStartNode();
},
startNode() {
return Math.max(0, this.firstVisibleNode - this.renderAhead);
},
lastVisibleNode() {
return this.findEndNode();
},
endNode() {
return Math.min(this.rowCount - 1, this.lastVisibleNode + this.renderAhead);
},
visibleNodeCount() {
return this.endNode - this.startNode + 1;
},
offsetY() {
return this.childPositions[this.startNode];
},
visibleItems() {
return this.items.slice(
this.startNode,
this.startNode + this.visibleNodeCount,
);
},
rowCount() {
return this.items.length;
},
spacerStyle() {
return {
transform: `translateY(${this.offsetY}px)`,
};
},
viewportStyle() {
return {
height: `${this.totalHeight}px`,
};
},
rootStyle() {
return {
height: `${this.rootHeight}px`,
};
},
},
methods: {
onScroll() {
window.requestAnimationFrame(() => {
this.scrollTop = this.$refs.root.scrollTop;
});
},
findStartNode() {
let startRange = 0;
let endRange = this.rowCount ? this.rowCount - 1 : this.rowCount;
while (endRange !== startRange) {
const middle = Math.floor((endRange - startRange) / 2 + startRange);
if (
this.childPositions[middle] <= this.scrollTop
&& this.childPositions[middle + 1] > this.scrollTop
) {
return middle;
}
if (middle === startRange) {
// edge case - start and end range are consecutive
return endRange;
}
if (this.childPositions[middle] <= this.scrollTop) {
startRange = middle;
} else {
endRange = middle;
}
}
return this.rowCount;
},
findEndNode() {
let endNode;
for (endNode = this.firstVisibleNode; endNode < this.rowCount; endNode++) {
if (this.childPositions[endNode] > this.childPositions[this.firstVisibleNode] + this.rootHeight) {
return endNode;
}
}
return endNode;
},
},
};
</script>
<style lang="scss" scoped>
.virtual-scroll {
overflow: auto;
&__spacer {
display: flex;
will-change: transform;
}
&__viewport {
position: relative;
width: 100%;
will-change: transform;
overflow: hidden;
}
}
</style>
@patchthecode
Copy link

Does this handle dynamic row heights?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment