/** * Main CRUD Manager - Coordinates everything */ class CRUDManager { constructor(config) { this.queue = window.jvbQueue; console.log(this.queue); this.config = config; this.content = config.content || false; if (!this.content) { return; } this.initElements(); this.updateBulkOptions(); // Initialize components this.store = new window.jvbStore({ name: this.content, endpoint: 'content', headers: { 'action_nonce': jvbSettings.dash, }, filters: { content: this.content, user: jvbSettings.currentUser, page: 1, status: 'all' } }); this.status = 'all'; this.filterTimeout = null; this.viewController = new window.jvbViews(this.ui.container, this.store); this.formController = new window.jvbForm(this.store); this.formController.subscribe((event, data) => { switch(event) { case 'form-submit': case 'form-autosave': this.handleFormChange(event,data); break; } }); if (window.jvbQueue) { window.jvbQueue.subscribe((event, data) => { if (event === 'operation-completed' && data.source === 'form') { this.handleQueueSuccess(event, data); } else if (event === 'operation-failed-permanent' && data.source === 'form') { this.handleQueueFailure(event, data); } }); } // Track initialization this.initialized = false; this.init(); } handleFormChange(event, data) { data.changes.content = this.content; let changes = {}; let title = ''; let itemsToRemove = []; switch (true) { case data.config.element === this.ui.forms.edit: let postID = data.config.id.replace('edit-', ''); console.log(postID); changes[postID] = data.changes; title = `Saving ${data.fullData['post_title']} Changes`; // Check if status change requires removal if (data.changes.post_status && this.shouldRemoveItem(data.changes.post_status)) { itemsToRemove.push(postID); } break; case data.config.element === this.ui.forms.bulkEdit: let selected = data.config.element.querySelectorAll('.selected input:checked'); selected.forEach(sel => { changes[sel.value] = data.changes; // Check if status change requires removal if (data.changes.post_status && this.shouldRemoveItem(data.changes.post_status)) { itemsToRemove.push(sel.value); } }); title = `Updating ${selected.length} ${this.config.plural??'posts'} Changes`; break; case data.config.element === this.ui.forms.create: if (event === 'form-submit') { changes[data.config.data['form-id']] = data.fullData; title = `Saving ${data.fullData['post_title']} Changes`; } break; } // Handle visual removal with stagger effect if (itemsToRemove.length > 0) { let delay = 0; itemsToRemove.forEach(itemId => { setTimeout(() => { const element = document.querySelector(`.item[data-id="${itemId}"]`); if (element) { window.fade(element, false); } }, delay); delay += 50; // Stagger by 50ms }); // Clear selection after bulk edit with staggered removal if (data.config.element === this.ui.forms.bulkEdit) { setTimeout(() => { this.viewController.clearSelection(); }, delay + 100); } } if (window.isEmptyObject(changes)) { return; } this.savePosts(changes, title); } shouldRemoveItem(newStatus) { return (this.status === 'all' && !['publish', 'draft'].includes(newStatus)) || (newStatus !== this.status); } savePosts(changes, title) { if (window.isEmptyObject(changes)) { return; } let operation = { endpoint: 'content', headers: { 'action_nonce': jvbSettings.dash, }, data: { posts: changes, }, popup: `Saving changes`, title: title }; this.queue.addToQueue(operation); } handleQueueSuccess(event, data) { console.log('Handling queue success...'); console.log('Event', event); console.log('Data', data); } handleQueueFailure(event, data) { console.log('Handling queue failure...'); console.log('Event', event); console.log('Data', data); } initElements() { this.elements = { modals: { create: 'dialog.create', edit: 'dialog.edit', bulkEdit: 'dialog.bulkEdit' }, container: '.crud[data-content]', grid: '.item-grid', bulkSelectActions: '.bulk-action-select', forms: { create: 'dialog.create form', edit: 'dialog.edit form', bulkEdit: 'dialog.bulkEdit form' } }; this.ui = window.uiFromSelectors(this.elements); } init() { // Set up filter controls this.filterHandler = this.handleFilterChange.bind(this); this.changeHandler = this.handleChange.bind(this); this.modals = {}; for (let [name, modal] of Object.entries(this.ui.modals)) { this.modals[name] = new window.jvbModal(modal); this.modals[name].subscribe((event, data) => { switch (event) { case 'modal-close': this.formController.cleanupForm(this.modals[name].modal.querySelector('form').dataset.formId); //double check we have finished saving console.log('Data on modal close: ', data); break; case 'modal-open': //probably not needed in this class break; } }); } // Set up global event delegation this.setupEventDelegation(); this.setupFilters(); // Load initial data this.store.fetch(); this.initialized = true; } setupEventDelegation() { document.addEventListener('change', this.changeHandler); // Single event listener for all CRUD actions document.addEventListener('click', (e) => { // Check for action buttons const actionBtn = e.target.closest('[data-action]'); if (actionBtn) { e.preventDefault(); const action = actionBtn.dataset.action; const id = actionBtn.dataset.id; switch(action) { case 'edit': this.populateEditForm(id); this.modals.edit.handleOpen(); break; case 'delete': if (confirm('Delete this item?')) { let changes = {}; changes[actionBtn.dataset.id] = { 'post_status': 'delete', 'content': this.content }; window.fade(actionBtn.closest('.item'), false); this.savePosts(changes, `Sending ${this.singular} to trash...`); this.store.deleteItem(id); } break; case 'trash': let changes = {}; changes[actionBtn.dataset.id] = { 'post_status': 'trash', 'content': this.content }; window.fade(actionBtn.closest('.item'), false); this.savePosts(changes, `Sending ${this.singular} to trash...`); break; case 'create': this.modals.create.dataset.itemID = 'new'; this.modals.create.dataset.content = this.content; this.modals.create.handleOpen(); break; case 'bulk-edit': const selected = Array.from(this.viewController.selectedItems); if (selected.length > 0) { this.modals.bulkEdit.handleOpen(); } break; case 'bulk-delete': const toDelete = Array.from(this.viewController.selectedItems); if (toDelete.length > 0 && confirm(`Delete ${toDelete.length} items?`)) { toDelete.forEach(id => this.store.deleteItem(id)); this.viewController.clearSelection(); } break; case 'sync': this.store.syncQueue(); break; case 'refresh': this.store.fetchFromServer(this.store.filters); break; } } let createButton = e.target.closest('.create-item'); if (createButton) { this.formController.registerForm(this.ui.forms.create); this.modals.create.handleOpen(); } let clearSelection = e.target.closest('.cancel-bulk'); if (clearSelection) { this.viewController.selectAll(false); } }); // Keyboard shortcuts document.addEventListener('keydown', (e) => { // Ctrl/Cmd + A to select all if ((e.ctrlKey || e.metaKey) && e.key === 'a') { if (this.ui.container && this.ui.container.contains(document.activeElement)) { e.preventDefault(); this.viewController.selectAll(); } } // ESC to clear selection if (e.key === 'Escape' && this.viewController?.selectedItems.size > 0 && window.jvbModal.getAllModals().length === 0) { this.viewController.clearSelection(); } }); } handleChange(e) { if (e.target.classList.contains('bulk-action-select')) { if (e.target.value.startsWith('tax-')) { const taxonomy = e.target.value.replace('tax-', ''); this.openTaxonomyModal(taxonomy); e.target.value = ''; return; } switch (e.target.value) { case 'edit': this.populateBulkEdit(); this.modals.bulkEdit.handleOpen(); break; case 'publish': this.setBulkStatus('publish'); break; case 'draft': this.setBulkStatus('draft'); break; case 'trash': this.setBulkStatus('trash'); break; case 'restore': this.setBulkStatus('draft'); break; case 'delete': this.setBulkStatus('delete'); break; } } } openTaxonomyModal(taxonomy) { // Check if jvbSelector exists if (!window.jvbSelector) { console.error('TaxonomySelector not initialized'); return; } // Open the selector in filter mode window.jvbSelector.openForFilter( taxonomy, (selectedIds, taxonomy) => this.handleBulkTaxonomy(selectedIds, taxonomy) ); } handleBulkTaxonomy(selectedIds, taxonomy) { console.log(taxonomy, selectedIds); // Callback when terms are selected if (selectedIds.length > 0) { selectedIds = selectedIds.join(','); let changes = {}; let selected = Array.from(this.viewController.selectedItems); console.log('selected',selected); selected.forEach(sel => { changes[sel] = { content: this.content }; changes[sel][taxonomy] = selectedIds; }); console.log('Taxonomy changes: ', changes); let title = `Adding ${selected.length} ${this.config.plural??'posts'} to ${selectedIds.length} ${jvbSettings.labels[taxonomy].plural}`; this.viewController.clearSelection(); this.savePosts(changes, title); } } setBulkStatus(status) { if (!['publish', 'draft', 'trash', 'delete'].includes(status)){ return; } console.log(`Setting status: ${status}`); let changes = {}; for (let selected of this.viewController.selectedItems) { changes[selected] = { post_status: status, content: this.content }; } let title; switch (status) { case 'delete': title = 'Deleting'; break; default: title = window.uppercaseFirst(status)+'ing'; } console.log(this.status); if ((this.status === 'all' && !['publish', 'draft'].includes(status)) || status !== this.status) { let delay = 0; for (let selected of this.viewController.selectedItems) { setTimeout(() => { const element = document.querySelector(`.item[data-id="${selected}"]`); if (element) { window.fade(element, false); } }, delay); delay += 50; // Increment delay for staggered effect } } // Clear selection even if items aren't being removed this.viewController.clearSelection(); if (!window.isEmptyObject(changes)) { this.savePosts(changes, `${title} ${this.viewController.selectedItems.size} ${this.plural}...`); } } handleFilterChange(e) { let target = e.target; let filter = target.dataset.filter; if (filter === 'taxonomies') { let taxonomy = target.dataset.taxonomy; this.store.setFilter(`tax_${taxonomy}`, filter.value); } else { this[target.dataset.filter] = target.value; this.store.setFilter(target.dataset.filter, target.value); if (target.dataset.filter === 'status') { this.updateBulkOptions(target.value); } } } updateBulkOptions(status = 'all') { if (status === 'trash') { if (this.ui.bulkSelectActions.querySelector('[value="edit"]')) { window.removeChildren(this.ui.bulkSelectActions); let options = window.getTemplate('trashOptions'); options.querySelectorAll('option').forEach((option, index) => { if (index === 0) { option.checked = true; } this.ui.bulkSelectActions.append(option); }); } } else { if (!this.ui.bulkSelectActions.querySelector('[value="edit"]')) { window.removeChildren(this.ui.bulkSelectActions); let options = window.getTemplate('notTrashOptions'); options.querySelectorAll('option').forEach((option, index) => { this.ui.bulkSelectActions.append(option); }); } } this.ui.bulkSelectActions.value = ''; } populateBulkEdit() { const container = this.modals.bulkEdit.modal.querySelector('form .selected'); if (!container) return; window.removeChildren(container); for (let selected of this.viewController.selectedItems) { console.log(selected); let item = this.store.getItem(selected); console.log(item); const img = window.getTemplate('bulkItem'); if (!img) return; const checkbox = img.querySelector('input[type=checkbox]'); const image = img.querySelector('img'); if (checkbox) { checkbox.id = `bulk_${item.id}`; checkbox.value = item.id; checkbox.checked = true; } if (image && item.thumbnail) { image.src = item.thumbnail; image.alt = item.alt || ''; } container.append(img); } let modal = this.modals.bulkEdit.modal; [ modal.querySelector('h2 span').textContent ] = [ this.viewController.selectedItems.size ]; this.formController.registerForm(this.ui.forms.bulkEdit); console.log('Bulk Edit form registered'); } populateEditForm(itemID) { let item = this.store.getItem(itemID); console.log(item); if (item) { this.ui.modals.edit.dataset.itemID = itemID; this.ui.modals.edit.dataset.content = this.content; let form = this.ui.modals.edit.querySelector('form'); [ this.ui.modals.edit.querySelector('h2').textContent ] = [ `Editing ${item.fields.post_title}` ]; form.dataset.formId = `edit-${itemID}`; console.log(form.dataset.formId); new window.jvbPopulate(form, item.fields, item.images); this.formController.registerForm(this.ui.forms.edit); console.log('Edit form registered'); } } setupFilters() { document.querySelectorAll('[data-filter]').forEach(filter => { filter.addEventListener('change', (e) => { if (this.filterTimeout) { clearTimeout(this.filterTimeout); } this.filterTimeout = setTimeout(() => { this.filterHandler(e); }, 300); }); }); // Search const searchInput = document.querySelector('input[type="search"]'); if (searchInput) { let searchTimeout; searchInput.addEventListener('input', () => { if (searchInput.value.length > 3) { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { this.store.setFilter('search', searchInput.value); }, 300); } else if (searchInput.value.length === 0) { this.store.removeFilter('search'); } }); } } destroy() { document.querySelectorAll('[data-filter]').forEach(filter => { filter.removeEventListener('change', this.filterHandler); }); this.store.subscribers.clear(); } } // Initialize when ready document.addEventListener('DOMContentLoaded', () => { let container = document.querySelector('[data-content]'); if (container) { window.crudManager = new CRUDManager({ content: container.dataset.content, }); } });