Skip to content

Instantly share code, notes, and snippets.

@mirzap
Created April 1, 2025 08:35
Show Gist options
  • Save mirzap/5454f3c515b82c463237e7aa1f1012d4 to your computer and use it in GitHub Desktop.
Save mirzap/5454f3c515b82c463237e7aa1f1012d4 to your computer and use it in GitHub Desktop.
Plain JavaScript large virtualized list with sorting and filtering
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Virtualized Events List with Sorting and Filtering</title>
<style>
body {
font-family: Arial, sans-serif;
}
#controls {
width: 500px;
margin: 20px auto;
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: space-between;
}
#controls > * {
flex: 1 1 45%;
}
#viewport {
height: 600px;
overflow-y: scroll;
position: relative;
border: 1px solid #ccc;
width: 500px;
margin: auto;
}
.item {
position: absolute;
left: 0;
right: 0;
height: 40px;
padding: 5px;
box-sizing: border-box;
border-bottom: 1px solid #eee;
}
</style>
</head>
<body>
<div id="controls">
<input type="text" id="searchInput" placeholder="Search by name, email or message">
<select id="sortSelect">
<option value="id">Sort by ID</option>
<option value="cc">Sort by Country Code</option>
<option value="plan">Sort by Plan</option>
<option value="size">Sort by Size</option>
</select>
<select id="planFilter">
<option value="">All Plans</option>
<option value="free">Free</option>
<option value="pro">Pro</option>
<option value="enterprise">Enterprise</option>
</select>
<select id="sizeFilter">
<option value="">All Sizes</option>
<option value="s">S</option>
<option value="m">M</option>
<option value="l">L</option>
<option value="xl">XL</option>
</select>
<button onclick="applyFiltersAndSort()">Apply</button>
</div>
<div id="viewport">
<div id="content"></div>
</div>
<script>
const viewport = document.getElementById('viewport');
const content = document.getElementById('content');
const itemHeight = 40;
const events = [];
let currentEntries = events;
function add_events(input) {
const lines = input.split('\n');
for (let line of lines) {
if (line.trim()) {
try {
events.push(JSON.parse(line.trim()));
} catch (e) {
console.warn('Skipping malformed line:', line);
}
}
}
currentEntries = events;
render();
}
function render() {
content.style.height = `${currentEntries.length * itemHeight}px`;
const scrollTop = viewport.scrollTop;
const viewportHeight = viewport.clientHeight;
const start = Math.floor(scrollTop / itemHeight);
const end = Math.min(currentEntries.length, start + Math.ceil(viewportHeight / itemHeight) + 10);
content.innerHTML = '';
for (let i = start; i < end; i++) {
const div = document.createElement('div');
div.className = 'item';
div.style.top = `${i * itemHeight}px`;
const event = currentEntries[i];
div.textContent = `${event.data.name} (${event.data.email}) - ${event.data.message}`;
content.appendChild(div);
}
}
viewport.addEventListener('scroll', render);
function sortEntries(sort_by = 'id', ascending = true) {
const planOrder = ['free', 'pro', 'enterprise'];
const sizeOrder = ['s', 'm', 'l', 'xl'];
currentEntries = [...currentEntries].sort((a, b) => {
let compare = 0;
if (sort_by === 'id') compare = a.data.id - b.data.id;
else if (sort_by === 'cc') compare = a.data.cc.localeCompare(b.data.cc);
else if (sort_by === 'size') compare = sizeOrder.indexOf(a.data.size || 's') - sizeOrder.indexOf(b.data.size || 's');
else if (sort_by === 'plan') compare = planOrder.indexOf(a.data.plan) - planOrder.indexOf(b.data.plan);
return ascending ? compare : -compare;
});
}
function filterEntries({ type, plan, size }) {
currentEntries = events.filter(el => {
return (!type || el.type === type) &&
(!plan || el.data.plan === plan) &&
(!size || el.data.size === size);
});
}
function search(query) {
const q = query.toLowerCase();
currentEntries = currentEntries.filter(event => {
return (event.data.message || '').toLowerCase().includes(q) ||
(event.data.name || '').toLowerCase().includes(q) ||
(event.data.email || '').toLowerCase().includes(q);
});
}
function applyFiltersAndSort() {
const sortBy = document.getElementById('sortSelect').value;
const plan = document.getElementById('planFilter').value;
const size = document.getElementById('sizeFilter').value;
const searchQuery = document.getElementById('searchInput').value;
currentEntries = [...events];
filterEntries({ plan, size });
sortEntries(sortBy);
if (searchQuery) {
search(searchQuery);
}
render();
}
// Simulate adding events
const simulatedInputLines = [];
for (let i = 0; i < 150000; i++) {
simulatedInputLines.push(JSON.stringify({
type: "question",
data: {
cc: "fr",
email: `user${i + 1}@example.com`,
id: i + 1,
message: `Message number ${i + 1}`,
name: `User #${i + 1}`,
plan: ['free', 'pro', 'enterprise'][i % 3],
size: ['s', 'm', 'l', 'xl'][i % 4]
}
}));
}
function addInChunks(lines, chunkSize = 5000) {
let index = 0;
function processChunk() {
const chunk = lines.slice(index, index + chunkSize);
add_events(chunk.join('\n'));
index += chunkSize;
if (index < lines.length) {
setTimeout(processChunk, 0);
}
}
processChunk();
}
addInChunks(simulatedInputLines);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment