diff --git a/dist/css/styles.css b/dist/css/styles.css index 600ca20..099baa4 100644 --- a/dist/css/styles.css +++ b/dist/css/styles.css @@ -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, diff --git a/dist/js/app.js b/dist/js/app.js index a804a4a..a353e77 100644 --- a/dist/js/app.js +++ b/dist/js/app.js @@ -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 = ` + + + + 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 = ` +
+ 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';