Created
April 1, 2025 08:35
-
-
Save mirzap/5454f3c515b82c463237e7aa1f1012d4 to your computer and use it in GitHub Desktop.
Plain JavaScript large virtualized list with sorting and filtering
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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