class BaseDashboardManager { constructor(config) { this.config = { apiNamespace: jvbSettings.api, selectors: { container: '.replace', grid: '[role="grid"]', statusFilters: '.status-filters', bulkControls: '.bulk-edit-controls', scrollSentinel: '.scroll-sentinel', savePopup: '.save-popup', selectAll: '#select-all', bulkActionSelect: '.bulk-action-select', selectedCount: '.selected-count' }, bulkEditMode: false, selectedItems: new Set(), page: 1, loading: false, hasMore: true, ...config }; // Initialize after DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.init()); } else { this.init(); } } init() { // Cache DOM elements this.container = document.querySelector(this.config.selectors.container); if (!this.container) { console.warn('Container element not found'); return; } this.grid = this.container.querySelector(this.config.selectors.grid); this.statusFilters = this.container.querySelector(this.config.selectors.statusFilters); this.bulkControls = this.container.querySelector(this.config.selectors.bulkControls); this.scrollSentinel = this.container.querySelector(this.config.selectors.scrollSentinel); this.savePopup = document.querySelector(this.config.selectors.savePopup); this.loadingManager = new LoadingManager(); this.initModalHandling(); // Initialize components only if required elements exist if (this.grid) { this.initStatusFilters(); this.initBulkEdit(); this.initInfiniteScroll(); this.initCache(); this.initModalHandling(); } } // Status filtering initStatusFilters() { if (!this.statusFilters) return; this.statusFilters.addEventListener('click', async e => { const button = e.target.closest('.status-filter'); if (!button) return; this.updateActiveFilter(button); await this.handleStatusChange(button.dataset.status); }); } updateActiveFilter(button) { this.statusFilters.querySelectorAll('.status-filter').forEach(btn => btn.classList.remove('active') ); button.classList.add('active'); } async handleStatusChange(newStatus) { this.config.currentStatus = newStatus; // Reset state this.page = 1; this.hasMore = true; this.selectedItems.clear(); if (this.bulkEditMode) { this.toggleBulkEdit(); } // Update view document.body.classList.remove('view-all', 'view-publish', 'view-draft', 'view-trash'); document.body.classList.add(`view-${newStatus}`); // Reload content await this.loadItems(); } // Bulk editing initBulkEdit() { if (!this.bulkControls) return; const selectAll = this.container.querySelector(this.config.selectors.selectAll); if (!selectAll) return; selectAll.addEventListener('change', () => { const visibleItems = this.getVisibleItems(); visibleItems.forEach(item => { item.classList.toggle('selected', selectAll.checked); if (selectAll.checked) { this.selectedItems.add(item.dataset.id); } else { this.selectedItems.delete(item.dataset.id); } }); this.updateBulkControls(); }); this.bulkControls.addEventListener('click', e => { const button = e.target.closest('button'); if (!button) return; switch (button.className) { case 'toggle-bulk-edit': this.toggleBulkEdit(); break; case 'bulk-edit': this.openBulkEditModal(); break; case 'bulk-delete': this.handleBulkDelete(); break; case 'bulk-restore': this.handleBulkRestore(); break; case 'cancel-bulk': this.toggleBulkEdit(); break; } }); // Add click handler for grid items in batch mode this.grid.addEventListener('click', e => { if (!this.bulkEditMode) return; const item = e.target.closest('li'); if (!item) return; const isSelected = item.classList.toggle('selected'); if (isSelected) { this.selectedItems.add(item.dataset.id); } else { this.selectedItems.delete(item.dataset.id); } this.updateSelectAllState(); this.updateBulkControls(); }); // Initialize bulk action handling const bulkActionSelect = this.container.querySelector(this.config.selectors.bulkActionSelect); const applyBulk = this.container.querySelector('.apply-bulk'); if (applyBulk && bulkActionSelect) { applyBulk.addEventListener('click', () => { const action = bulkActionSelect.value; if (!action || this.selectedItems.size === 0) return; switch (action) { case 'trash': this.handleBulkDelete(); break; case 'publish': this.handleBulkStatusChange('publish'); break; case 'draft': this.handleBulkStatusChange('draft'); break; } }); } // Handle escape key document.addEventListener('keydown', e => { if (e.key === 'Escape' && this.bulkEditMode) { this.toggleBulkEdit(); } }); // Initialize long press handling this.initLongPress(); } async handleBulkStatusChange(status) { if (this.selectedItems.size === 0) return; try { this.loadingManager.show( `Updating ${this.selectedItems.size} tattoos...`, 'Updating', this.config.quips.update ); const results = await this.processBatchOperation('status', Array.from(this.selectedItems), { status } ); // Update UI for changed items results.forEach(result => { if (result.success) { const item = this.grid.querySelector(`[data-id="${result.id}"]`); if (item) { item.className = status; } } }); this.showNotification( `${this.selectedItems.size} tattoos updated successfully` ); } catch (error) { console.error('Bulk status update error:', error); this.showNotification('Error updating tattoos', 'error'); } finally { this.loadingManager.hide(); this.toggleBulkEdit(); } } updateSelectAllState() { const selectAll = this.container.querySelector(this.config.selectors.selectAll); if (!selectAll) return; const visibleItems = this.getVisibleItems(); const checkedCount = visibleItems.filter(item => this.selectedItems.has(item.dataset.id) ).length; if (checkedCount === 0) { selectAll.checked = false; selectAll.indeterminate = false; } else if (checkedCount === visibleItems.length) { selectAll.checked = true; selectAll.indeterminate = false; } else { selectAll.checked = false; selectAll.indeterminate = true; } } updateBulkControls() { const bulkActions = this.container.querySelector('.bulk-actions'); if (!bulkActions) return; const hasSelection = this.selectedItems.size > 0; bulkActions.hidden = !hasSelection; // Update selected count const countEl = this.container.querySelector(this.config.selectors.selectedCount); if (countEl) { countEl.textContent = hasSelection ? `${this.selectedItems.size} selected` : ''; } // Toggle batch-mode class on body document.body.classList.toggle('batch-mode', this.bulkEditMode); } clearSelection() { this.selectedItems.clear(); this.getVisibleItems().forEach(item => item.classList.remove('selected') ); const selectAll = this.container.querySelector(this.config.selectors.selectAll); if (selectAll) { selectAll.checked = false; selectAll.indeterminate = false; } this.updateBulkControls(); } getVisibleItems() { return Array.from( this.grid.querySelectorAll('li:not([hidden])') ); } initLongPress() { // Configuration for long press this.longPressConfig = { delay: 500, // Time in ms to trigger long press startTime: 0, startTarget: null, timer: null, touchStarted: false }; // Add touch event listeners this.grid.addEventListener('touchstart', (e) => this.handleTouchStart(e), { passive: true }); this.grid.addEventListener('touchend', () => this.handleTouchEnd()); this.grid.addEventListener('touchmove', () => this.handleTouchMove()); // Add mouse event listeners this.grid.addEventListener('mousedown', (e) => this.handleMouseDown(e)); this.grid.addEventListener('mouseup', () => this.handleMouseUp()); this.grid.addEventListener('mouseleave', () => this.handleMouseUp()); } handleTouchStart(e) { if (this.currentStatus === 'trash') return; // Don't activate in trash view const item = e.target.closest('li'); if (!item) return; this.longPressConfig.touchStarted = true; this.startLongPress(item); } handleTouchEnd() { this.longPressConfig.touchStarted = false; this.cancelLongPress(); } handleTouchMove() { if (this.longPressConfig.touchStarted) { this.cancelLongPress(); } } handleMouseDown(e) { if (this.currentStatus === 'trash') return; // Don't activate in trash view // Only handle left mouse button if (e.button !== 0) return; const item = e.target.closest('li'); if (!item) return; this.startLongPress(item); } handleMouseUp() { this.cancelLongPress(); } startLongPress(item) { this.longPressConfig.startTime = Date.now(); this.longPressConfig.startTarget = item; // Clear any existing timer if (this.longPressConfig.timer) { clearTimeout(this.longPressConfig.timer); } // Set new timer this.longPressConfig.timer = setTimeout(() => { if (!this.bulkEditMode) { this.toggleBulkEdit(); // Select the long-pressed item this.toggleItemSelection(item); // Add visual feedback item.classList.add('long-press-activated'); setTimeout(() => { item.classList.remove('long-press-activated'); }, 200); } }, this.longPressConfig.delay); } cancelLongPress() { if (this.longPressConfig.timer) { clearTimeout(this.longPressConfig.timer); } this.longPressConfig.startTime = 0; this.longPressConfig.startTarget = null; } toggleBulkEdit() { this.bulkEditMode = !this.bulkEditMode; document.body.classList.toggle('batch-mode', this.bulkEditMode); if (!this.bulkEditMode) { this.selectedItems.clear(); this.grid.querySelectorAll('.selected').forEach(item => item.classList.remove('selected') ); this.updateSelectedCount(); } } updateSelectedCount() { const count = this.selectedItems.size; this.container.querySelector('.selected-count').textContent = count ? `${count} selected` : ''; } // Infinite scrolling initInfiniteScroll() { if (!this.scrollSentinel) return; const observer = new IntersectionObserver( entries => { entries.forEach(entry => { if (entry.isIntersecting && !this.loading && this.hasMore) { this.loadMoreItems(); } }); }, { rootMargin: '300px' } ); observer.observe(this.scrollSentinel); } // Caching initCache() { this.cache = { data: {}, metadata: {}, maxAge: 5 * 60 * 1000, // 5 minutes }; } getCachedData(status, page) { const cached = this.cache.data[status]?.[page]; if (!cached) return null; const metadata = this.cache.metadata[status]; if (!metadata || Date.now() - metadata.lastUpdated > this.cache.maxAge) { return null; } return cached; } updateCache(status, page, data) { if (!this.cache.data[status]) { this.cache.data[status] = {}; this.cache.metadata[status] = { lastUpdated: Date.now(), totalPages: 1 }; } this.cache.data[status][page] = data; this.cache.metadata[status].lastUpdated = Date.now(); this.cache.metadata[status].totalPages = Math.max( this.cache.metadata[status].totalPages, page ); } clearCache() { this.cache.data = {}; this.cache.metadata = {}; } // Modal handling initModalHandling() { // Implemented by child classes } // API interaction async fetchWithError(endpoint, options = {}) { const defaultOptions = { credentials: 'same-origin', headers: { 'X-WP-Nonce': jvbSettings.nonce, 'Content-Type': 'application/json' } }; try { const response = await fetch( `${this.config.apiNamespace}${endpoint}`, { ...defaultOptions, ...options } ); if (!response.ok) { const data = await response.json().catch(() => ({})); throw new Error(data.message || `HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error('API Error:', error); throw error; } } // Loading state showLoading(message, title, quips) { this.loadingManager.show(message, title, quips); this.grid.classList.add('loading'); } hideLoading() { this.loadingManager.hide(); this.grid.classList.remove('loading'); } // Must be implemented by child classes async loadItems() { throw new Error('loadItems must be implemented by child class'); } async loadMoreItems() { throw new Error('loadMoreItems must be implemented by child class'); } async handleBulkDelete() { throw new Error('handleBulkDelete must be implemented by child class'); } async handleBulkRestore() { throw new Error('handleBulkRestore must be implemented by child class'); } } // Export managers window.BaseDashboardManager = BaseDashboardManager;