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;
|