/**
|
* Main CRUD Manager - Coordinates everything
|
*/
|
class CRUDManagerOld {
|
constructor(config) {
|
this.queue = window.jvbQueue;
|
this.config = config;
|
this.content = config.content || false;
|
this.settings = window.jvbUserSettings;
|
this.a11y = window.jvbA11y;
|
if (!this.content) {
|
return;
|
}
|
this.isTimeline = false;
|
this.currentItemID = null;
|
|
this.initElements();
|
this.updateBulkOptions();
|
|
// Initialize components
|
const store = window.jvbStore.register(
|
this.content,
|
{
|
storeName: this.content,
|
keyPath: 'id',
|
endpoint: 'content',
|
headers: {
|
'X-Action-Nonce': window.auth.getNonce('dash'),
|
},
|
indexes: [
|
{name: 'id', keyPath: 'id'},
|
{ name: 'status', keyPath: 'status'},
|
{ name: 'date', keyPath: 'date'},
|
{ name: 'modified', keyPath: 'modified'},
|
{ name: 'title', keyPath: 'title'}
|
],
|
filters: {
|
content: this.content,
|
user: window.auth.getUser(),
|
page: 1,
|
status: 'all',
|
orderby: 'modified', //or title
|
order: 'desc'
|
},
|
TTL: 30 * 60 * 1000, //30 minutes cache
|
showLoading: true,
|
});
|
this.store = store[this.content];
|
|
this.status = 'all';
|
this.filterTimeout = null;
|
|
this.viewController = new window.jvbViews(this.ui.container, this.store);
|
this.tableForm = null;
|
this.tableChanges = new Map();
|
|
|
this.formController = new window.jvbForm();
|
this.viewController.subscribe((event, form) => {
|
if (event === 'table-view' && !this.tableForm) {
|
if (!this.tableForm) {
|
this.tableForm = this.formController.registerForm(form, {
|
autosave: false,
|
formStatus: false,
|
isTable: true,
|
});
|
}
|
|
} else if (event === 'not-table-view') {
|
if (this.tableForm) {
|
|
}
|
} else if (event === 'order-changed') {
|
let data = this.store.get(form);
|
if (!data) {
|
return;
|
}
|
let changes = {};
|
changes[form] = data;
|
this.savePosts(changes, `Updating progression order`);
|
}
|
});
|
|
this.formController.subscribe((event, data) => {
|
switch(event) {
|
case 'form-submit':
|
case 'form-autosave':
|
this.handleFormChange(event,data);
|
break;
|
}
|
});
|
|
this.queue.subscribe((event, data) => {
|
if (!Object.hasOwn(data, 'endpoint') || !['content', 'uploads/groups'].includes(data.endpoint)) return;
|
if (event === 'operation-completed') {
|
this.handleQueueSuccess(event, data);
|
} else if (event === 'operation-failed-permanent') {
|
this.handleQueueFailure(event, data);
|
}
|
});
|
|
|
// Track initialization
|
this.initialized = false;
|
|
this.init();
|
}
|
handleFormChange(event, data) {
|
let title = data.fullData.post_title;
|
let changes = (Object.hasOwn(data, 'changes')) ? data.changes : data.fullData;
|
|
let theChanges = {};
|
if (this.isTimeline) {
|
theChanges[this.currentItemID] = changes;
|
this.savePosts(theChanges, title);
|
return;
|
}
|
|
let itemsToRemove = [];
|
console.log(data);
|
switch (true) {
|
case data.config.element === this.ui.forms.edit:
|
theChanges[this.currentItemID] = changes;
|
title = `Saving ${title} Changes`;
|
// Check if status change requires removal
|
if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
|
itemsToRemove.push(this.currentItemID);
|
}
|
break;
|
case data.config.element === this.ui.forms.bulkEdit:
|
let selected = data.config.element.querySelectorAll('.selected input:checked');
|
selected.forEach(sel => {
|
theChanges[sel.value] = changes;
|
// Check if status change requires removal
|
if (changes.post_status && this.shouldRemoveItem(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') {
|
theChanges[data.config.data['form-id']] = changes;
|
title = `Saving ${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 (Object.keys(theChanges).length === 0) {
|
return;
|
}
|
|
this.savePosts(theChanges, title);
|
}
|
|
shouldRemoveItem(newStatus) {
|
return (this.status === 'all' && !['publish', 'draft'].includes(newStatus)) ||
|
(newStatus !== this.status);
|
}
|
|
savePosts(changes, title) {
|
if (Object.keys(changes).length === 0) {
|
return;
|
}
|
|
//ensure content is in each post
|
for (let postId in changes) {
|
if (!changes[postId]['content']) {
|
changes[postId]['content'] = this.content;
|
}
|
}
|
let operation = {
|
endpoint: 'content',
|
headers: {
|
'X-Action-Nonce': window.auth.getNonce('dash'),
|
},
|
data: {
|
posts: changes,
|
},
|
delay: true,
|
popup: `Saving changes`,
|
title: title
|
};
|
|
this.queue.addToQueue(operation);
|
|
}
|
async handleQueueSuccess(event, data) {
|
this.store.clearCache();
|
this.store.fetch();
|
}
|
handleQueueFailure(event, data) {
|
console.error('Operation failed permanently:', data);
|
// Optionally show error notification to user
|
this.a11y?.announce(`Operation failed: ${data.error_message || 'Unknown error'}`);
|
}
|
|
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'
|
},
|
uploader: 'details.uploader'
|
};
|
this.ui = window.uiFromSelectors(this.elements);
|
if (this.ui.uploader) {
|
window.jvbUploads.scanFields(document.querySelector(this.elements.uploader));
|
|
window.jvbUploads.subscribe((event, data) => {
|
if (event === 'sent-to-queue') {
|
console.log(data);
|
if (data === this.ui.uploader.querySelector('[data-uploader]')?.dataset.uploader) {
|
window.debouncer.schedule('crud-complete', ()=> {
|
this.store.clearCache();
|
});
|
}
|
}
|
});
|
}
|
this.isTimeline = !!document.querySelector('[data-timeline]');
|
}
|
init() {
|
if (this.ui.uploader){
|
this.settings.addSetting(this.ui.uploader, 'open');
|
this.ui.uploader.addEventListener('toggle', (e) =>{
|
this.settings.saveSetting('open', this.ui.uploader.open ? 'on' : 'off');
|
});
|
}
|
|
// 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.currentItemID = null;
|
this.formController.cleanupForm(this.modals[name].modal.querySelector('form').dataset.formId);
|
//double check we have finished saving
|
break;
|
case 'modal-open':
|
//probably not needed in this class
|
break;
|
}
|
});
|
}
|
|
// Set up global event delegation
|
this.setupEventDelegation();
|
|
this.setupFilters();
|
|
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.closest('[data-id]')) {
|
if (this.isTimeline) {
|
this.handleTimelineTableChange(e);
|
} else {
|
this.handleTableChange(e);
|
}
|
return;
|
}
|
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;
|
}
|
}
|
if (window.targetCheck(e, 'select[data-filter]')) {
|
this.handleFilterChange(e);
|
}
|
}
|
handleTableChange(e) {
|
const row = e.target.closest('tr[data-id]');
|
if (!row) return;
|
|
const input = e.target;
|
const postID = parseInt(row.dataset.id);
|
const fieldName = input.closest(['data-field'])?.dataset.field;
|
if (!fieldName) return;
|
|
const item = this.store.get(postID);
|
if (!item) return;
|
|
item.fields[fieldName] = this.getInputValue(input);
|
|
this.store.save(item);
|
|
let post = {};
|
post[postID] = item.fields;
|
this.savePosts(post, `Saving changes to ${this.content}`);
|
}
|
handleTimelineTableChange(e) {
|
const tbody = e.target.closest('tbody[data-id]');
|
if (!tbody) return;
|
|
const input = e.target;
|
const fieldName = input.closest('[data-field]')?.dataset.field;
|
|
if (!fieldName) return;
|
|
const parentID = parseInt(tbody.dataset.id);
|
const timelinePoint = input.closest('tr.timeline-point');
|
|
const item = this.store.get(parentID);
|
if (!item) return;
|
|
const value = this.getInputValue(input);
|
|
// Check if this is a specific point, or a shared value
|
if (timelinePoint) {
|
const imgID = timelinePoint.dataset.imageId;
|
if (!item.fields.timeline) {
|
item.fields.timeline = {};
|
}
|
if (!item.fields.timeline[imgID]) {
|
item.fields.timeline[imgID] = {};
|
}
|
item.fields.timeline[imgID][fieldName] = value;
|
} else {
|
item.fields[fieldName] = value;
|
}
|
|
//Update store directly
|
this.store.save(item);
|
|
let changes = {};
|
changes[parentID] = item.fields;
|
this.savePosts(changes, 'Updating progress post');
|
}
|
getInputValue(input) {
|
if (input.type === 'checkbox') {
|
return input.checked ? (input.value || '1') : '';
|
}
|
if (input.type === 'radio') {
|
return input.checked ? input.value : null;
|
}
|
return input.value;
|
}
|
|
collectTimelineData(form) {
|
const formData = new FormData(form);
|
const data = {};
|
const timeline = [];
|
|
// Get the base content type
|
data.content = form.closest('dialog')?.dataset.content || this.content;
|
data.user = window.auth.getUser();
|
|
// Collect timeline entries
|
const timelineGroups = form.querySelectorAll('.upload-group');
|
|
timelineGroups.forEach((group, index) => {
|
const groupId = group.dataset.groupId;
|
const entry = {
|
id: group.dataset.itemId || 'new',
|
order: index
|
};
|
|
// Get group-specific fields
|
const titleInput = group.querySelector(`[name="${groupId}[post_title]"], [name="${groupId}_post_title"]`);
|
const excerptInput = group.querySelector(`[name="${groupId}[post_excerpt]"], [name="${groupId}_post_excerpt"]`);
|
|
if (titleInput) entry.post_title = titleInput.value;
|
if (excerptInput) entry.post_excerpt = excerptInput.value;
|
|
// Get images in this group
|
const images = Array.from(group.querySelectorAll('.item-grid.group .item[data-upload-id]'));
|
entry.images = images.map(img => img.dataset.uploadId);
|
|
// Check for featured image
|
const featuredInput = group.querySelector('[name*="featured"]:checked');
|
if (featuredInput) {
|
entry.featured = featuredInput.value;
|
}
|
|
timeline.push(entry);
|
});
|
|
// Get ungrouped uploads (if any)
|
const mainGrid = form.querySelector('.item-grid.preview');
|
if (mainGrid) {
|
const ungroupedImages = Array.from(mainGrid.querySelectorAll('.item[data-upload-id]'));
|
ungroupedImages.forEach(img => {
|
timeline.push({
|
id: 'new',
|
images: [img.dataset.uploadId]
|
});
|
});
|
}
|
|
// Collect other form fields (shared fields)
|
for (let [key, value] of formData.entries()) {
|
// Skip group-specific fields
|
if (!key.includes('[') && !key.includes('_group_') && key !== 'timeline') {
|
data[key] = value;
|
}
|
}
|
|
data.timeline = timeline;
|
|
return data;
|
}
|
|
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) {
|
// Callback when terms are selected
|
if (selectedIds.length > 0) {
|
selectedIds = selectedIds.join(',');
|
let changes = {};
|
let selected = Array.from(this.viewController.selectedItems);
|
|
|
selected.forEach(sel => {
|
changes[sel] = {
|
content: this.content
|
};
|
changes[sel][taxonomy] = selectedIds;
|
});
|
|
|
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;
|
}
|
|
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';
|
}
|
|
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 (Object.keys(changes).length !== 0) {
|
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}`, target.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 && !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);
|
});
|
}
|
}
|
if (this.ui.bulkSelectActions) {
|
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) {
|
let item = this.store.get(selected);
|
|
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);
|
}
|
|
populateEditForm(itemID) {
|
this.currentItemID = itemID;
|
|
let item = this.store.get(parseInt(itemID));
|
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}`;
|
|
new window.jvbPopulate(form, item);
|
|
this.formController.registerForm(this.ui.forms.edit);
|
}
|
}
|
|
setupFilters() {
|
// Search
|
const searchInput = document.querySelector('.all-filters 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);
|
});
|
}
|
}
|
|
// Initialize when ready
|
document.addEventListener('DOMContentLoaded', async function() {
|
window.auth.subscribe((event) => {
|
if (event === 'auth-loaded') {
|
let container = document.querySelector('[data-content]');
|
if (container && !Object.hasOwn(container.dataset, 'ignore')) {
|
window.crudManager = new CRUDManager({
|
content: container.dataset.content,
|
});
|
}
|
}
|
});
|
});
|