/**
|
* 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,
|
storeName: this.content,
|
endpoint: 'content',
|
headers: {
|
'action_nonce': jvbSettings.dash,
|
},
|
indexes: [
|
{ name: 'status', keyPath: 'post_status'},
|
{ name: 'modified', keyPath: 'modified'},
|
],
|
filters: {
|
content: this.content,
|
user: jvbSettings.currentUser,
|
page: 1,
|
status: 'all'
|
},
|
TTL: 3600000,
|
cacheDOM: true
|
});
|
|
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.queue.subscribe((event, data) => {
|
switch (event) {
|
case 'operation-status':
|
//update items?
|
break;
|
}
|
});
|
|
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.delete(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.delete(id));
|
this.viewController.clearSelection();
|
}
|
break;
|
|
case 'sync':
|
// this.store.syncQueue();
|
break;
|
|
case 'refresh':
|
this.store.fetch();
|
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.get(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.get(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,
|
});
|
}
|
|
});
|