feat: implement "Load All" button with lazy loading functionality for improved user experience
This commit is contained in:
parent
4cf024c44b
commit
1bf25d3878
2 changed files with 185 additions and 10 deletions
66
dist/css/styles.css
vendored
66
dist/css/styles.css
vendored
|
|
@ -1469,6 +1469,72 @@ button:focus-visible {
|
|||
margin-left: var(--space-sm)
|
||||
}
|
||||
|
||||
/* ===== LOAD ALL BUTTON ===== */
|
||||
.load-all-container {
|
||||
text-align: center;
|
||||
padding: var(--space-2xl) 0;
|
||||
margin-top: var(--space-xl);
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.load-all-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-lg) var(--space-2xl);
|
||||
font-size: var(--text-base);
|
||||
font-weight: var(--font-semibold);
|
||||
gap: var(--space-sm);
|
||||
transition: all var(--transition-fast);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15);
|
||||
}
|
||||
|
||||
.load-all-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.25);
|
||||
}
|
||||
|
||||
.load-all-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.load-all-btn svg {
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.load-all-btn:hover svg {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.load-all-btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.load-all-container {
|
||||
padding: var(--space-xl) var(--space-md);
|
||||
}
|
||||
|
||||
.load-all-btn {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
select,
|
||||
|
|
|
|||
129
dist/js/app.js
vendored
129
dist/js/app.js
vendored
|
|
@ -347,12 +347,15 @@ class TableManager {
|
|||
this.#elements = {
|
||||
container: document.getElementById(containerId),
|
||||
searchInput: document.getElementById(searchInputId),
|
||||
clearIcon: document.getElementById(clearIconId)
|
||||
clearIcon: document.getElementById(clearIconId),
|
||||
loadAllBtn: null
|
||||
};
|
||||
|
||||
this.options = {
|
||||
sortable: true,
|
||||
chunkSize: CONFIG.PERFORMANCE.RENDER_CHUNK_SIZE,
|
||||
initialLimit: 30, // Show only first 30 entries initially
|
||||
enableLazyLoad: true, // Enable lazy loading feature
|
||||
...options
|
||||
};
|
||||
|
||||
|
|
@ -361,7 +364,9 @@ class TableManager {
|
|||
filteredData: {},
|
||||
sort: { column: null, direction: 'asc' },
|
||||
services: [],
|
||||
columns: []
|
||||
columns: [],
|
||||
isFullyLoaded: false,
|
||||
isSearchActive: false
|
||||
});
|
||||
|
||||
this.#init();
|
||||
|
|
@ -403,7 +408,9 @@ class TableManager {
|
|||
currentData: data,
|
||||
filteredData: { ...data },
|
||||
services: services || DataTransformer.extractServices(data),
|
||||
columns: DataTransformer.extractServices(data)
|
||||
columns: DataTransformer.extractServices(data),
|
||||
isFullyLoaded: false,
|
||||
isSearchActive: false
|
||||
});
|
||||
|
||||
this.#renderTable();
|
||||
|
|
@ -476,8 +483,8 @@ class TableManager {
|
|||
const tbody = table?.querySelector('tbody');
|
||||
if (!tbody) return;
|
||||
|
||||
const { filteredData, columns } = this.#state.get();
|
||||
const entries = Object.entries(filteredData);
|
||||
const { filteredData, columns, isFullyLoaded, isSearchActive } = this.#state.get();
|
||||
let entries = Object.entries(filteredData);
|
||||
|
||||
// Cancel any pending render
|
||||
if (this.#idleCallbackId) {
|
||||
|
|
@ -496,11 +503,22 @@ class TableManager {
|
|||
return;
|
||||
}
|
||||
|
||||
// Render in chunks
|
||||
this.#renderChunked(tbody, entries, columns, token);
|
||||
// Limit entries if not fully loaded and not searching
|
||||
const shouldLimit = this.options.enableLazyLoad && !isFullyLoaded && !isSearchActive;
|
||||
const displayLimit = shouldLimit ? this.options.initialLimit : entries.length;
|
||||
|
||||
// If limiting, only show first N entries
|
||||
if (shouldLimit && entries.length > displayLimit) {
|
||||
entries = entries.slice(0, displayLimit);
|
||||
// Render in chunks
|
||||
this.#renderChunked(tbody, entries, columns, token, true);
|
||||
} else {
|
||||
// Render in chunks
|
||||
this.#renderChunked(tbody, entries, columns, token, false);
|
||||
}
|
||||
}
|
||||
|
||||
#renderChunked(tbody, entries, columns, token) {
|
||||
#renderChunked(tbody, entries, columns, token, showLoadMore) {
|
||||
let index = 0;
|
||||
const chunkSize = this.options.chunkSize;
|
||||
|
||||
|
|
@ -522,12 +540,87 @@ class TableManager {
|
|||
// Continue rendering if more entries exist
|
||||
if (index < entries.length) {
|
||||
this.#idleCallbackId = Utils.scheduleIdleWork(renderNextChunk);
|
||||
} else if (showLoadMore) {
|
||||
// After rendering limited entries, show the "Load All" button
|
||||
this.#showLoadAllButton();
|
||||
}
|
||||
};
|
||||
|
||||
this.#idleCallbackId = Utils.scheduleIdleWork(renderNextChunk);
|
||||
}
|
||||
|
||||
#showLoadAllButton() {
|
||||
// Remove existing button if any
|
||||
this.#removeLoadAllButton();
|
||||
|
||||
const { filteredData } = this.#state.get();
|
||||
const totalEntries = Object.keys(filteredData).length;
|
||||
const displayedEntries = this.options.initialLimit;
|
||||
const remainingEntries = totalEntries - displayedEntries;
|
||||
|
||||
// Create button container
|
||||
const buttonContainer = Utils.createElement('div', {
|
||||
className: 'load-all-container',
|
||||
style: 'text-align: center; padding: var(--space-2xl) 0; margin-top: var(--space-xl);'
|
||||
});
|
||||
|
||||
// Create button
|
||||
const loadAllBtn = Utils.createElement('button', {
|
||||
className: 'btn btn-primary load-all-btn',
|
||||
type: 'button'
|
||||
});
|
||||
|
||||
loadAllBtn.innerHTML = `
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 8px;">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
Load All ${totalEntries} Hosts (${remainingEntries} more)
|
||||
`;
|
||||
|
||||
loadAllBtn.addEventListener('click', () => this.#loadAllEntries(), { once: true });
|
||||
|
||||
buttonContainer.appendChild(loadAllBtn);
|
||||
|
||||
// Store reference
|
||||
this.#elements.loadAllBtn = buttonContainer;
|
||||
|
||||
// Insert after table wrapper
|
||||
const wrapper = this.#elements.container.querySelector('.table-wrapper');
|
||||
if (wrapper && wrapper.nextSibling) {
|
||||
this.#elements.container.insertBefore(buttonContainer, wrapper.nextSibling);
|
||||
} else {
|
||||
this.#elements.container.appendChild(buttonContainer);
|
||||
}
|
||||
}
|
||||
|
||||
#removeLoadAllButton() {
|
||||
if (this.#elements.loadAllBtn) {
|
||||
this.#elements.loadAllBtn.remove();
|
||||
this.#elements.loadAllBtn = null;
|
||||
}
|
||||
}
|
||||
|
||||
#loadAllEntries() {
|
||||
// Update state to show all entries
|
||||
this.#state.update({ isFullyLoaded: true });
|
||||
|
||||
// Show loading indicator on button
|
||||
const btn = this.#elements.loadAllBtn?.querySelector('.load-all-btn');
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `
|
||||
<div class="loading-spinner" style="width: 20px; height: 20px; margin-right: 8px;"></div>
|
||||
Loading...
|
||||
`;
|
||||
}
|
||||
|
||||
// Re-render table body with all entries
|
||||
setTimeout(() => {
|
||||
this.#removeLoadAllButton();
|
||||
this.#renderTableBody();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
#createTableRow(host, hostData, columns) {
|
||||
const row = Utils.createElement('tr', {
|
||||
role: 'row',
|
||||
|
|
@ -677,7 +770,15 @@ class TableManager {
|
|||
)
|
||||
);
|
||||
|
||||
this.#state.set('filteredData', filtered);
|
||||
// When searching, mark as search active and load all
|
||||
this.#state.update({
|
||||
filteredData: filtered,
|
||||
isSearchActive: true,
|
||||
isFullyLoaded: true // Auto-load all when searching
|
||||
});
|
||||
|
||||
// Remove load all button if present
|
||||
this.#removeLoadAllButton();
|
||||
|
||||
if (this.#elements.clearIcon) {
|
||||
this.#elements.clearIcon.style.display = 'block';
|
||||
|
|
@ -697,7 +798,15 @@ class TableManager {
|
|||
}
|
||||
|
||||
const { currentData } = this.#state.get();
|
||||
this.#state.set('filteredData', { ...currentData });
|
||||
|
||||
// When clearing search, reset to limited view if was initially limited
|
||||
const shouldResetToLimited = this.options.enableLazyLoad;
|
||||
|
||||
this.#state.update({
|
||||
filteredData: { ...currentData },
|
||||
isSearchActive: false,
|
||||
isFullyLoaded: shouldResetToLimited ? false : true
|
||||
});
|
||||
|
||||
if (this.#elements.clearIcon) {
|
||||
this.#elements.clearIcon.style.display = 'none';
|
||||
|
|
|
|||
Loading…
Reference in a new issue