mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 01:31:08 -05:00
Add comment and reply functionality with preview and notification templates
This commit is contained in:
190
static/js/components/virtual-scroller.js
Normal file
190
static/js/components/virtual-scroller.js
Normal file
@@ -0,0 +1,190 @@
|
||||
// Virtual Scroller Component
|
||||
// Implements efficient scrolling for large lists by only rendering visible items
|
||||
|
||||
class VirtualScroller {
|
||||
constructor(options) {
|
||||
this.container = options.container;
|
||||
this.itemHeight = options.itemHeight;
|
||||
this.bufferSize = options.bufferSize || 5;
|
||||
this.renderItem = options.renderItem;
|
||||
this.items = [];
|
||||
this.scrollTop = 0;
|
||||
this.visibleItems = new Map();
|
||||
|
||||
this.observer = new IntersectionObserver(
|
||||
this._handleIntersection.bind(this),
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
this._setupContainer();
|
||||
this._bindEvents();
|
||||
}
|
||||
|
||||
_setupContainer() {
|
||||
if (!this.container) {
|
||||
throw new Error('Container element is required');
|
||||
}
|
||||
|
||||
this.container.style.position = 'relative';
|
||||
this.container.style.height = '600px'; // Default height
|
||||
this.container.style.overflowY = 'auto';
|
||||
|
||||
// Create spacer element to maintain scroll height
|
||||
this.spacer = document.createElement('div');
|
||||
this.spacer.style.position = 'absolute';
|
||||
this.spacer.style.top = '0';
|
||||
this.spacer.style.left = '0';
|
||||
this.spacer.style.width = '1px';
|
||||
this.container.appendChild(this.spacer);
|
||||
}
|
||||
|
||||
_bindEvents() {
|
||||
this.container.addEventListener(
|
||||
'scroll',
|
||||
this._debounce(this._handleScroll.bind(this), 16)
|
||||
);
|
||||
|
||||
// Handle container resize
|
||||
if (window.ResizeObserver) {
|
||||
const resizeObserver = new ResizeObserver(this._debounce(() => {
|
||||
this._render();
|
||||
}, 16));
|
||||
resizeObserver.observe(this.container);
|
||||
}
|
||||
}
|
||||
|
||||
setItems(items) {
|
||||
this.items = items;
|
||||
this.spacer.style.height = `${items.length * this.itemHeight}px`;
|
||||
this._render();
|
||||
}
|
||||
|
||||
_handleScroll() {
|
||||
this.scrollTop = this.container.scrollTop;
|
||||
this._render();
|
||||
}
|
||||
|
||||
_handleIntersection(entries) {
|
||||
entries.forEach(entry => {
|
||||
const itemId = entry.target.dataset.itemId;
|
||||
if (!entry.isIntersecting) {
|
||||
this.visibleItems.delete(itemId);
|
||||
entry.target.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_render() {
|
||||
const visibleRange = this._getVisibleRange();
|
||||
const itemsToRender = new Set();
|
||||
|
||||
// Calculate which items should be visible
|
||||
for (let i = visibleRange.start; i <= visibleRange.end; i++) {
|
||||
if (i >= 0 && i < this.items.length) {
|
||||
itemsToRender.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove items that are no longer visible
|
||||
for (const [itemId] of this.visibleItems) {
|
||||
const index = parseInt(itemId);
|
||||
if (!itemsToRender.has(index)) {
|
||||
const element = this.container.querySelector(`[data-item-id="${itemId}"]`);
|
||||
if (element) {
|
||||
this.observer.unobserve(element);
|
||||
element.remove();
|
||||
}
|
||||
this.visibleItems.delete(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new visible items
|
||||
for (const index of itemsToRender) {
|
||||
if (!this.visibleItems.has(index.toString())) {
|
||||
this._renderItem(index);
|
||||
}
|
||||
}
|
||||
|
||||
// Update performance metrics
|
||||
this._updateMetrics(itemsToRender.size);
|
||||
}
|
||||
|
||||
_renderItem(index) {
|
||||
const item = this.items[index];
|
||||
const element = document.createElement('div');
|
||||
|
||||
element.style.position = 'absolute';
|
||||
element.style.top = `${index * this.itemHeight}px`;
|
||||
element.style.left = '0';
|
||||
element.style.width = '100%';
|
||||
element.dataset.itemId = index.toString();
|
||||
|
||||
// Render content
|
||||
element.innerHTML = this.renderItem(item);
|
||||
|
||||
// Add to container and observe
|
||||
this.container.appendChild(element);
|
||||
this.observer.observe(element);
|
||||
this.visibleItems.set(index.toString(), element);
|
||||
|
||||
// Adjust actual height if needed
|
||||
const actualHeight = element.offsetHeight;
|
||||
if (actualHeight !== this.itemHeight) {
|
||||
element.style.height = `${this.itemHeight}px`;
|
||||
}
|
||||
}
|
||||
|
||||
_getVisibleRange() {
|
||||
const start = Math.floor(this.scrollTop / this.itemHeight) - this.bufferSize;
|
||||
const visibleCount = Math.ceil(this.container.clientHeight / this.itemHeight);
|
||||
const end = start + visibleCount + 2 * this.bufferSize;
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
_updateMetrics(visibleCount) {
|
||||
const metrics = {
|
||||
totalItems: this.items.length,
|
||||
visibleItems: visibleCount,
|
||||
scrollPosition: this.scrollTop,
|
||||
containerHeight: this.container.clientHeight,
|
||||
renderTime: performance.now() // You can use this with the previous render time
|
||||
};
|
||||
|
||||
// Dispatch metrics event
|
||||
this.container.dispatchEvent(new CustomEvent('virtualScroller:metrics', {
|
||||
detail: metrics
|
||||
}));
|
||||
}
|
||||
|
||||
_debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// Public methods
|
||||
scrollToIndex(index) {
|
||||
if (index >= 0 && index < this.items.length) {
|
||||
this.container.scrollTop = index * this.itemHeight;
|
||||
}
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this._render();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.observer.disconnect();
|
||||
this.container.innerHTML = '';
|
||||
this.items = [];
|
||||
this.visibleItems.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export default VirtualScroller;
|
||||
Reference in New Issue
Block a user