class ContentManager {
|
constructor(config) {
|
// Core configuration
|
this.config = {
|
content: '',
|
plural: '',
|
taxonomies: {},
|
selectors: {
|
container: '.items-list',
|
grid: '.item-grid:not(.preview)',
|
uploadZone: '.file-upload-wrapper',
|
statusFilters: '.status-filters',
|
dateFilters: '.date-filters',
|
taxonomyFilters: '.taxonomy-filters',
|
viewControls: '.view-controls',
|
bulkControls: '.bulk-controls',
|
scrollSentinel: '.scroll-sentinel',
|
editModal: '.edit-modal',
|
bulkEditModal: '.bulk-edit-modal',
|
clearButton: '.clear-filters',
|
},
|
createPostPerFile: true,
|
uploadConfig: {
|
mode: 'direct',
|
allowMultiple: true,
|
createPostPerFile: true,
|
maxSize: 5242880, // 5MB
|
allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
},
|
...config
|
};
|
|
this.resetCache = false;
|
|
// Initialize managers
|
this.queueManager = window.jvbQueue;
|
this.loadingManager = window.jvbLoading;
|
this.cache = window.jvbCache;
|
this.error = window.jvbError;
|
|
// State management
|
this.state = {
|
selected: new Set(),
|
filters: {
|
status: 'all',
|
taxonomies: {},
|
date: null
|
},
|
view: localStorage.getItem(`${this.config.content}_view`) || 'grid',
|
loading: false,
|
};
|
|
this.queue = {
|
all: {
|
items: new Map(),
|
page: 1,
|
hasMore: true,
|
totalPages: 0
|
},
|
draft: {
|
items: new Map(),
|
page: 1,
|
hasMore: true,
|
totalPages: 0
|
},
|
publish: {
|
items: new Map(),
|
page: 1,
|
hasMore: true,
|
totalPages: 0
|
},
|
trash: {
|
items: new Map(),
|
page: 1,
|
hasMore: true,
|
totalPages: 0
|
}
|
};
|
|
this.init();
|
}
|
|
async init() {
|
// Cache DOM elements
|
this.elements = {};
|
Object.entries(this.config.selectors).forEach(([key, selector]) => {
|
this.elements[key] = document.querySelector(selector);
|
});
|
// Initialize file uploader if needed
|
if (this.config.uploadConfig) {
|
this.fileUploader = new window.jvbFileUploader({
|
...this.config.uploadConfig,
|
content: this.config.content,
|
fieldName: null,
|
});
|
}
|
|
this.initStatusFilters();
|
this.initDateFilters();
|
this.initTaxonomyFilters();
|
this.initClearFilters();
|
this.initViewControls();
|
this.initBulkControls();
|
this.initInfiniteScroll();
|
this.initModals();
|
|
// Load initial content
|
await this.loadContent();
|
|
}
|
|
queueContentUpdate(postID, data) {
|
// Structure the operation for the queue
|
const operation = {
|
type: 'content_update',
|
data: {
|
posts: {
|
[postID]: {
|
content: this.config.content,
|
...data
|
}
|
},
|
content: this.config.content
|
}
|
};
|
|
// Add to queue manager
|
this.queueManager.addToQueue(operation);
|
|
// Update local state optimistically
|
this.updateLocalState(postID, data);
|
}
|
|
queueBulkUpdate(postIDs, data) {
|
// Structure posts data
|
const posts = {};
|
postIDs.forEach(id => {
|
posts[id] = {
|
content: this.config.content,
|
...data
|
};
|
});
|
|
const operation = {
|
user: window.auth.getUser(),
|
type: 'content_update',
|
data: {
|
posts: posts
|
}
|
};
|
|
this.queueManager.addToQueue(operation);
|
|
// Update local state optimistically
|
postIDs.forEach(id => this.updateLocalState(id, data));
|
}
|
|
updateLocalState(postID, data) {
|
const item = this.queue[this.state.filters.status].items.get(postID);
|
if (item) {
|
// Merge new data with existing item
|
Object.assign(item, data);
|
this.queue[this.state.filters.status].items.set(postID, item);
|
|
// Update UI
|
const element = this.elements.grid.querySelector(`[data-id="${postID}"]`);
|
if (element) {
|
this.updateItemElement(element, item);
|
}
|
}
|
}
|
|
processFormData(formData) {
|
const data = {};
|
|
// Process basic fields
|
for (const [key, value] of formData.entries()) {
|
if (key === 'status') {
|
data.status = value;
|
} else if (key.startsWith('taxonomy_')) {
|
// Handle taxonomy data
|
const taxName = key.replace('taxonomy_', '');
|
if (!data.taxonomies) data.taxonomies = {};
|
data.taxonomies[taxName] = Array.isArray(value) ? value : [value];
|
} else {
|
// Handle regular fields
|
data[key] = value;
|
}
|
}
|
|
return data;
|
}
|
|
// Update UI elements
|
updateItemElement(element, item) {
|
// Update status classes
|
element.classList.remove('draft', 'publish', 'trash');
|
element.classList.add(item.status);
|
|
// Update status icon
|
const statusIcon = element.querySelector('.action-status');
|
if (statusIcon) {
|
removeChildren(statusIcon);
|
statusIcon.append(getIcon(item.status));
|
}
|
|
// Update taxonomies display
|
if (item.taxonomies) {
|
const taxGroups = element.querySelectorAll('.label-group');
|
taxGroups.forEach(group => {
|
const taxName = group.dataset.taxonomy;
|
if (taxName && item.taxonomies[taxName]) {
|
const terms = item.taxonomies[taxName].terms;
|
group.querySelector('.terms').innerHTML = this.renderTerms(terms);
|
}
|
});
|
}
|
}
|
|
handleItemAction(action, itemElement) {
|
const itemId = itemElement.dataset.id;
|
|
switch(action) {
|
case 'edit':
|
this.editModal.handleOpen();
|
this.openEditModal(itemElement);
|
if(this.editModal.form){
|
new FormFields(this.editModal.form, {
|
onSave: this.editModal.onSave(),
|
itemID: itemElement.dataset.id,
|
});
|
}
|
break;
|
case 'restore':
|
this.queueContentUpdate(itemId, {
|
status: 'draft'
|
});
|
itemElement.remove();
|
break;
|
case 'trash':
|
// In other views - move to trash
|
this.queueContentUpdate(itemId, {
|
status: 'trash'
|
});
|
itemElement.remove();
|
break;
|
case 'delete':
|
if (confirm(`Hold up! Are you sure you want to permanently delete this ${this.config.content}?\n\nThis is a forever kind of deal - no taking it back.`)) {
|
this.queueContentUpdate(itemId, {
|
status: 'delete'
|
});
|
itemElement.remove();
|
}
|
|
break;
|
|
case 'toggle-status':
|
const currentStatus = itemElement.dataset.status;
|
const newStatus = currentStatus === 'publish' ? 'draft' : 'publish';
|
|
this.queueContentUpdate(itemId, {
|
status: newStatus
|
});
|
|
// Visual feedback
|
itemElement.dataset.status = newStatus;
|
|
removeChildren(itemElement.querySelector('.action-status'));
|
itemElement.querySelector('.action-status').append(getIcon(newStatus));
|
break;
|
}
|
}
|
|
async handleBulkOperation(status, postIDs) {
|
|
window.jvbLoading.show('Processing bulk changes...');
|
try {
|
// Create posts object with the same structure as queueContentUpdate
|
const posts = {};
|
postIDs.forEach(id => {
|
posts[id] = {
|
content: this.config.content,
|
status: status // Use the operation as the status
|
};
|
if(['delete', 'trash', 'restore'].includes(status)){
|
document.querySelector('[data-id="'+id+'"]').remove();
|
}
|
|
});
|
|
// Queue bulk operation with correct structure
|
this.queueManager.addToQueue({
|
type: 'content_update',
|
data: {
|
posts: posts
|
}
|
});
|
|
this.clearSelection();
|
this.showNotification('Bulk changes queued for processing');
|
} catch (error) {
|
console.error('Bulk operation failed:', error);
|
this.showNotification('Failed to queue bulk operation', 'error');
|
} finally {
|
window.jvbLoading.hide();
|
}
|
}
|
|
getQueryKey() {
|
return JSON.stringify({
|
status: this.state.filters.status,
|
page: this.state.page,
|
filters: this.state.filters
|
});
|
}
|
|
toggleItemSelection(item, selected) {
|
const id = item.dataset.id;
|
if (selected) {
|
this.state.selected.add(id);
|
item.classList.add('selected');
|
item.querySelector('input[type=checkbox]').checked = true;
|
} else {
|
this.state.selected.delete(id);
|
item.classList.remove('selected');
|
item.querySelector('input[type=checkbox]').checked = false;
|
}
|
}
|
|
// Content Loading and Rendering
|
async loadContent(reset = true) {
|
if (this.state.loading) return;
|
try {
|
this.state.loading = true;
|
this.loadingManager.show();
|
|
const status = this.state.filters.status;
|
console.log('Loading Page: ');
|
console.log(this.queue[status].page);
|
|
const params = new URLSearchParams();
|
params.set('type', this.config.content);
|
params.set('page', this.queue[status].page);
|
params.set('filters', JSON.stringify(this.state.filters));
|
params.set('user', window.auth.getUser());
|
|
if (reset) {
|
this.queue[status].page = 1;
|
this.queue[status].items.clear();
|
removeChildren(this.elements.grid);
|
this.elements.grid.classList.remove('empty');
|
|
}
|
let items;
|
|
|
const data = await this.cache.fetchWithCache(
|
`${jvbSettings.api}content?` + params,
|
{
|
method: 'GET',
|
headers: {
|
'Content-Type': 'application/json',
|
'X-WP-Nonce': window.auth.getNonce(),
|
'X-Action-Nonce': window.auth.getNonce('dash'),
|
},
|
},
|
{
|
context: window.auth.getUser()+'-'+this.config.content,
|
forceRefresh: false,
|
}
|
);
|
// const response = await fetch(`${jvbSettings.api}${jvbSettings.endpoints.get}?` + params, {
|
// method: 'GET',
|
// headers: {
|
// 'Content-Type': 'application/json',
|
// 'X-WP-Nonce': window.auth.getNonce(),
|
// 'X-Action-Nonce': window.auth.getNonce('dash'),
|
// }
|
// });
|
// const data = await response.json();
|
|
if (data.total > 0) {
|
this.elements.grid.classList.remove('empty');
|
data.items.forEach(item => {
|
this.queue[status].items.set(item.id, item);
|
});
|
this.queue[status].page++;
|
this.queue[status].totalPages = data.total_pages;
|
this.queue[status].hasMore = this.queue[status].page < data.total_pages;
|
} else {
|
this.elements.grid.classList.add('empty');
|
this.elements.grid.innerHTML = `<div class="empty-state"><h3>${jvbSettings.icons[this.config.content]}Nothing here${jvbSettings.icons[this.config.content]}</h3><p>It doesn't look like you have any ${this.config.plural} yet.</p><p><small><i>Add some by uploading images above.</i></small></p></div>`;
|
this.queue[status].page = 1;
|
this.queue[status].hasMore = false;
|
}
|
// }
|
|
|
|
this.renderContent();
|
|
} catch (error) {
|
console.error('Error loading content:', error);
|
this.loadingManager.showError('Failed to load content');
|
} finally {
|
this.state.loading = false;
|
this.loadingManager.hide();
|
}
|
}
|
|
renderContent() {
|
const currentStatus = this.state.filters.status;
|
const currentItems = this.queue[currentStatus].items;
|
// If we have items, ensure empty state is removed
|
if (currentItems.size > 0) {
|
this.elements.grid.classList.remove('empty');
|
if (this.elements.grid.querySelector('.empty-state')) {
|
removeChildren(this.elements.grid)
|
}
|
}
|
const fragment = document.createDocumentFragment();
|
|
currentItems.forEach(item => {
|
// Check if element already exists in the DOM
|
const existingElement = this.elements.grid.querySelector(`[data-id="${item.id}"]`);
|
|
if (existingElement) {
|
// If element exists but view changed, replace it
|
if (item.view !== this.state.view) {
|
const newElement = this.createItemElement(item);
|
item.view = this.state.view;
|
existingElement.replaceWith(newElement);
|
}
|
} else {
|
// Create new element if it doesn't exist
|
const element = this.createItemElement(item);
|
item.view = this.state.view;
|
fragment.appendChild(element);
|
}
|
|
this.queue[currentStatus].items.set(item.id, item);
|
});
|
|
// Only append fragment if it has children
|
if (fragment.children.length > 0) {
|
this.elements.grid.appendChild(fragment);
|
}
|
}
|
|
createItemElement(item) {
|
let itemEl = window.getTemplate(this.state.view+'View');
|
|
itemEl.classList.add(item.status);
|
itemEl.dataset.id = item.id;
|
itemEl.dataset.fields = JSON.stringify(item.fields);
|
itemEl.dataset.status = item.status;
|
itemEl.dataset.img = item.thumbnail;
|
|
let gallery = itemEl.querySelector('.gallery');
|
if(item.images){
|
itemEl.dataset.images = item.images;
|
let img = gallery.querySelector('img');
|
for(var image of item.images){
|
let newImg = img.cloneNode(true);
|
newImg.src = image.src;
|
if(image.alt){
|
newImg.alt = image.alt;
|
}
|
gallery.appendChild(
|
newImg
|
);
|
}
|
img.remove();
|
}else{
|
gallery.remove();
|
}
|
|
let taxonomies = [];
|
let itemTaxonomies = itemEl.querySelector('.taxonomies');
|
let itemTax = itemTaxonomies.querySelector('.label-group');
|
let taxLabel = itemTax.querySelector('.tax');
|
let hasTaxonomies = false;
|
for(let tax in item.taxonomies){
|
if(Object.keys(item.taxonomies[tax].terms).length > 0) {
|
hasTaxonomies = true;
|
itemEl.dataset[tax] = JSON.stringify(item.taxonomies[tax].terms);
|
let t = itemTax.cloneNode(true);
|
let icon = jvbSettings.icons[tax];
|
t.innerHTML = icon+t.innerHTML;
|
|
t.querySelector('.screen-reader-text').textContent = item.taxonomies[tax].name;
|
for(var term in item.taxonomies[tax].terms){
|
let label = taxLabel.cloneNode(true);
|
label.textContent = term.name;
|
t.appendChild(label);
|
}
|
}else{
|
itemEl.dataset[tax] = JSON.stringify({});
|
}
|
taxonomies.push(tax);
|
}
|
if(hasTaxonomies){
|
itemTax.remove();
|
taxLabel.remove();
|
}else{
|
itemTaxonomies.remove();
|
}
|
|
|
|
if(Object.keys(this.config.taxonomies).length === 0){
|
this.config.taxonomies = taxonomies;
|
}
|
|
let img = itemEl.querySelector('img');
|
img.src = item.thumbnail;
|
if(item.alt){
|
img.alt = item.alt;
|
}
|
|
let date = itemEl.querySelector('.date');
|
date.textContent = formatDate(item.date);
|
|
let title = 'Hide '+item.icon;
|
if(item.status === 'draft'){
|
title = 'Show '+item.icon;
|
}
|
let toggle = itemEl.querySelector('button[data-action="toggle-status"]');
|
toggle.prepend(getIcon(item.status));
|
toggle.title = title;
|
|
|
|
this.initItemEventListeners(itemEl);
|
return itemEl;
|
}
|
|
initItemEventListeners(element) {
|
// Selection handling
|
element.addEventListener('click', (e) => {
|
if (e.target.closest('.item-select')) {
|
e.preventDefault();
|
this.toggleItemSelection(element, !element.classList.contains('selected'));
|
this.updateBulkControls();
|
return;
|
}
|
|
// Handle edit click
|
if (e.target.closest('.action')) {
|
e.preventDefault();
|
this.handleItemAction(e.target.closest('.action').dataset.action, element);
|
return;
|
}
|
});
|
}
|
|
// render_grid_item(item){
|
// let html;
|
// switch(this.config.content){
|
// case 'tattoo':
|
// case 'artwork':
|
// html = `<summary>
|
// <div class="item-select">
|
// <input type="checkbox" class="select-checkbox" id="item-${item.id}" value="${item.id}">
|
// <label for="item-${item.id}">
|
// <span class="screen-reader-text">Select this ${this.config.content}</span>
|
// </label>
|
// </div>
|
// <img src="${item.thumbnail}" alt="${item.alt}">
|
// ${this.render_item_actions(item)}
|
// </summary>
|
// <div class="item-info">`;
|
//
|
// html += `<div class="label-group">
|
// ${dashboardSettings.icons.calendar}<span class="screen-reader-text">Date published</span>
|
// <span>${item.date}</span></div>`;
|
// for(let tax in item.taxonomies){
|
// if(Object.keys(item.taxonomies[tax].terms).length > 0){
|
// tax = item.taxonomies[tax];
|
// let terms = Object.entries(tax.terms);
|
//
|
// html += `<div class="label-group">
|
// ${jvbSettings.icons[tax.icon]}<span class="screen-reader-text">${tax.name}</span>`;
|
// terms.forEach(term => {
|
// html += `<span class="label">${term[1]}</span>`;
|
// });
|
// html += `</div>`;
|
// }
|
// };
|
// html += `</div>`;
|
// return html;
|
// break;
|
// }
|
// }
|
//
|
// render_list_item(item){
|
// let html;
|
// switch(this.config.content){
|
// case 'tattoo':
|
// case 'artwork':
|
// html = `
|
// <div class="item-select">
|
// <input type="checkbox" class="select-checkbox" id="item-${item.id}" value="${item.id}">
|
// <label for="item-${item.id}">
|
// <span class="screen-reader-text">Select this ${this.config.content}</span>
|
// </label>
|
// </div>
|
// <img src="${item.thumbnail}" alt="${item.alt}">
|
// ${this.render_item_actions(item)}
|
// <div class="item-info">`;
|
// html += `<div class="label-group">
|
// ${dashboardSettings.icons.calendar}<span class="screen-reader-text">Date published</span>
|
// <span>${item.date}</span></div>`;
|
// for(let tax in item.taxonomies){
|
// if(Object.keys(item.taxonomies[tax].terms).length > 0){
|
// tax = item.taxonomies[tax];
|
// let terms = Object.entries(tax.terms);
|
//
|
// html += `<div class="label-group">
|
// ${tax.icon}<span class="screen-reader-text">${tax.name}</span>`;
|
// terms.forEach(term => {
|
// html += `<span class="label">${term[1]}</span>`;
|
// });
|
// html += `</div>`;
|
// }
|
// };
|
// html += `</div>`;
|
// html += `</div>`;
|
// return html;
|
// break;
|
// }
|
// }
|
|
initInfiniteScroll() {
|
if (!this.elements.scrollSentinel) return;
|
const observer = new IntersectionObserver(entries => {
|
entries.forEach(entry => {
|
if (entry.isIntersecting && this.queue[this.state.filters.status].hasMore) {
|
this.loadContent(false);
|
}
|
});
|
});
|
|
observer.observe(this.elements.scrollSentinel);
|
}
|
|
|
// Filtering & Views
|
initStatusFilters() {
|
const statusContainer = this.elements.container.querySelector('.controls');
|
if (!statusContainer) return;
|
|
statusContainer.addEventListener('change', e => {
|
if (e.target.type === 'radio' && e.target.name === 'status-filters') {
|
const newStatus = e.target.id;
|
if (newStatus !== this.state.filters.status) {
|
this.state.filters.status = newStatus;
|
this.updateBulkActionOptions();
|
// Check if we already have items for this status
|
const queue = this.queue[newStatus];
|
if (queue.items.size === 0) {
|
// Load fresh if we don't have items
|
this.loadContent(true);
|
} else {
|
// Just re-render if we do
|
this.renderContent();
|
}
|
}
|
}
|
});
|
}
|
|
initDateFilters() {
|
const dateFilter = this.elements.container.querySelector('select.date-filter');
|
const dateRange = this.elements.container.querySelector('.date-range');
|
let lastValue;
|
if (dateFilter) {
|
this.hasFilters = true;
|
dateFilter.addEventListener('change', (e) => {
|
const value = e.target.value;
|
lastValue = value;
|
|
if (value === 'custom') {
|
dateRange.showModal();
|
return;
|
}
|
|
|
|
dateRange.close();
|
|
// Clear month select if exists
|
const monthSelect = dateRange.querySelector('.month-select');
|
if (monthSelect) monthSelect.value = '';
|
this.setDateFilter(value);
|
});
|
dateFilter.addEventListener('click', (e) => {
|
if (lastValue === 'custom' && dateFilter.value === 'custom') {
|
dateRange.showModal();
|
}
|
});
|
}
|
|
|
// Initialize custom date range
|
if (dateRange) {
|
const startInput = dateRange.querySelector('.date-start');
|
const endInput = dateRange.querySelector('.date-end');
|
const monthSelect = dateRange.querySelector('.month-select');
|
|
// Month select handler
|
if (monthSelect) {
|
monthSelect.addEventListener('change', (e) => {
|
const [year, month] = e.target.value.split('-');
|
if (year && month) {
|
const start = new Date(year, month - 1, 1);
|
const end = new Date(year, month, 0);
|
end.setHours(23, 59, 59, 999);
|
|
this.setDateFilter('custom', start, end);
|
dateRange.close();
|
}
|
});
|
}
|
|
// Custom date range handler
|
const updateDateRange = () => {
|
const start = startInput.value;
|
const end = endInput.value;
|
|
if (start && end) {
|
const startDate = new Date(start);
|
const endDate = new Date(end);
|
endDate.setHours(23, 59, 59, 999);
|
|
this.setDateFilter('custom', startDate, endDate);
|
dateRange.close();
|
}
|
};
|
|
startInput.addEventListener('change', updateDateRange);
|
endInput.addEventListener('change', updateDateRange);
|
}
|
}
|
|
setDateFilter(type, startDate = null, endDate = null) {
|
const now = new Date();
|
now.setHours(23, 59, 59, 999);
|
|
let start = startDate;
|
let end = endDate || now;
|
|
if (!startDate && type !== '') {
|
start = new Date();
|
switch(type) {
|
case 'today':
|
start.setHours(0, 0, 0, 0);
|
break;
|
case 'week':
|
start.setDate(now.getDate() - 7);
|
break;
|
case 'month':
|
start.setMonth(now.getMonth() - 1);
|
break;
|
case 'year':
|
start.setFullYear(now.getFullYear() - 1);
|
break;
|
}
|
}
|
|
this.state.filters.date = type ? {
|
range: {
|
after: start.toISOString(),
|
before: end.toISOString()
|
},
|
custom: type === 'custom'
|
} : {
|
range: null,
|
custom: false
|
};
|
|
this.updateClearFiltersButton();
|
this.state.page = 1;
|
this.loadContent();
|
}
|
|
initTaxonomyFilters() {
|
const filters = this.elements.container.querySelectorAll('.filter[data-taxonomy]');
|
if (!filters.length) return;
|
this.hasFilters = true;
|
filters.forEach(filter => {
|
filter.addEventListener('change', (e) => {
|
const taxonomy = e.target.dataset.taxonomy;
|
const value = e.target.value;
|
|
if (value) {
|
this.state.filters.taxonomies[taxonomy] = [parseInt(value)];
|
} else {
|
delete this.state.filters.taxonomies[taxonomy];
|
}
|
|
// Reset pagination and reload
|
this.updateClearFiltersButton();
|
this.state.page = 1;
|
this.loadContent(true);
|
});
|
});
|
}
|
|
updateClearFiltersButton() {
|
const button = document.querySelector(this.config.selectors.clearButton);
|
if (!button) return;
|
|
const hasFilters =
|
Object.keys(this.state.filters.taxonomies).length > 0 ||
|
this.state.filters.date.range !== null;
|
|
button.hidden = !hasFilters;
|
}
|
|
clearAllFilters() {
|
// Reset taxonomy filters
|
const filters = this.elements.container.querySelectorAll('.filter[data-taxonomy]');
|
filters.forEach(filter => filter.value = '');
|
|
// Reset date filter
|
const dateFilter = this.elements.container.querySelector('select.date-filter');
|
if (dateFilter) dateFilter.value = '';
|
|
// Reset state
|
this.state.filters = {
|
date: { range: null, custom: false },
|
taxonomies: {}
|
};
|
|
this.updateClearFiltersButton();
|
this.state.page = 1;
|
this.loadContent(true);
|
}
|
|
initClearFilters(){
|
if(this.config.selectors.clearButton){
|
document.querySelector(this.config.selectors.clearButton).addEventListener('click', () => this.clearAllFilters());
|
}
|
}
|
|
initViewControls() {
|
const viewContainer = this.elements.container.querySelector('.view-controls');
|
if (!viewContainer) return;
|
|
// Listen for radio button changes
|
viewContainer.addEventListener('change', e => {
|
const radio = e.target;
|
if (radio.type === 'radio') {
|
this.setView(radio.value);
|
this.loadContent(true); // Reload items with new view
|
}
|
});
|
|
// Set initial view
|
const savedView = localStorage.getItem(`${this.config.content}_view`) || 'grid';
|
const defaultRadio = viewContainer.querySelector(`input[value="${savedView}"]`);
|
if (defaultRadio) {
|
defaultRadio.checked = true;
|
this.setView(savedView);
|
}
|
}
|
|
setView(view) {
|
this.state.view = view;
|
|
// Store current selection state before clearing grid
|
const selectedItems = new Set(this.state.selected);
|
|
// Update grid class
|
this.elements.grid.classList.remove('grid-view', 'list-view');
|
this.elements.grid.classList.add(`${view}-view`);
|
|
// Store preference
|
localStorage.setItem(`${this.config.content}_view`, view);
|
|
this.loadContent(true);
|
|
|
// Restore selection state after re-rendering
|
selectedItems.forEach(id => {
|
const item = this.elements.grid.querySelector(`[data-id="${id}"]`);
|
if (item) {
|
const checkbox = item.querySelector('input[type="checkbox"]');
|
if (checkbox) {
|
checkbox.checked = true;
|
item.classList.add('selected');
|
}
|
}
|
});
|
// Update bulk controls to reflect selection state
|
this.updateBulkControls();
|
|
}
|
|
// Bulk Operations
|
initBulkControls() {
|
if (!this.elements.bulkControls) return;
|
|
// Select all handler
|
this.selectAll = this.elements.bulkControls.querySelector('.select-all');
|
if (this.selectAll) {
|
this.selectAll.addEventListener('change', () => {
|
const items = this.getVisibleItems();
|
items.forEach(item => {
|
this.toggleItemSelection(item, this.selectAll.checked);
|
});
|
this.updateBulkControls();
|
});
|
}
|
|
// Bulk actions handler
|
const bulkActionSelect = this.elements.bulkControls.querySelector('.bulk-action-select');
|
const applyButton = this.elements.bulkControls.querySelector('.apply-bulk');
|
|
if (applyButton && bulkActionSelect) {
|
this.updateBulkActionOptions();
|
const statusFilters = this.elements.container.querySelector('.status-filters');
|
|
applyButton.addEventListener('click', () => {
|
const action = bulkActionSelect.value;
|
if (!action) return;
|
|
const selectedIds = Array.from(this.state.selected);
|
|
switch (action) {
|
case 'restore':
|
this.handleBulkOperation('restore', selectedIds);
|
break;
|
case 'delete':
|
// Show confirmation for permanent deletion
|
if (confirm(`Hold up! Are you sure you want to permanently delete these ${this.config.plural}?\n\nThis is a forever kind of deal - no taking it back.`)) {
|
this.handleBulkOperation('delete', selectedIds);
|
}
|
break;
|
case 'trash':
|
this.handleBulkOperation('trash', selectedIds);
|
break;
|
case 'edit':
|
|
this.openBulkEditModal();
|
// Open bulk edit modal
|
const bulkEditModal = document.querySelector('.bulk-edit-modal');
|
if (bulkEditModal) {
|
const countSpan = bulkEditModal.querySelector('.selected-count');
|
if (countSpan) {
|
countSpan.textContent = `( ${selectedIds.length} items )`;
|
}
|
const items = bulkEditModal.querySelector('.selected');
|
if(items){
|
let content ='';
|
selectedIds.forEach(id=>{
|
let item = this.elements.grid.querySelector('[data-id="'+id+'"]');
|
content += '<input type="checkbox" id="selected-'+id+'" name="posts" value="'+id+'" checked><label for="selected-'+id+'"><img width="100%" height="auto" src="'+item.dataset.img+'"></label>';
|
});
|
items.innerHTML = content;
|
}
|
// if(bulkEditModal.querySelector('.taxonomies')){
|
// let taxonomies = bulkEditModal.querySelectorAll('.taxonomies .jvb-selector');
|
// taxonomies.forEach(taxonomy => {
|
// let tax = taxonomy.dataset.taxonomy.replace('e_','');
|
// let hierarchical = taxonomy.classList.contains('hierarchical');
|
// let config = JSON.parse(taxonomy.dataset.config);
|
//
|
// let selector;
|
// if(hierarchical) {
|
// selector = new NestedSelector(taxonomy, {
|
// title: 'Select '+tax+'(s)',
|
// allowMultiple: config.multiple,
|
// base: 'bulk-',
|
// });
|
// }else{
|
// selector = new BaseSelector(taxonomy, {
|
// title: 'Select '+tax+'(s)',
|
// allowMultiple: config.multiple,
|
// base: 'bulk-',
|
// });
|
// }
|
//
|
// });
|
// }
|
}
|
break;
|
case 'publish':
|
case 'draft':
|
this.handleBulkOperation(action, selectedIds);
|
break;
|
}
|
|
bulkActionSelect.value = ''; // Reset select
|
});
|
}
|
|
// Cancel bulk selection
|
const cancelButton = this.elements.bulkControls.querySelector('.cancel-bulk');
|
if (cancelButton) {
|
cancelButton.addEventListener('click', () => {
|
this.clearSelection();
|
});
|
}
|
}
|
|
// Add new method to update bulk action options
|
updateBulkActionOptions() {
|
const bulkActionSelect = this.elements.bulkControls.querySelector('.bulk-action-select');
|
if (!bulkActionSelect) return;
|
|
if (this.state.filters.status === 'trash') {
|
bulkActionSelect.innerHTML = `
|
<option value="">Bulk Actions...</option>
|
<option value="restore">Restore</option>
|
<option value="delete">Permanently Delete</option>
|
`;
|
} else {
|
bulkActionSelect.innerHTML = `
|
<option value="">Bulk Actions...</option>
|
<option value="edit">Edit</option>
|
<option value="publish">Show</option>
|
<option value="draft">Hide</option>
|
<option value="trash">Scrap</option>
|
`;
|
}
|
}
|
|
// Editing & Saving
|
initModals() {
|
if(this.elements.editModal){
|
this.editModal = new window.jvbModal(this.elements.editModal,{
|
open:false,
|
close: this.elements.editModal.querySelector('.cancel'),
|
save: this.elements.editModal.querySelector('.save'),
|
onSave: () => {
|
const formData = new FormData(this.elements.editModal.querySelector('form'));
|
let taxonomies = {};
|
const taxonomySelectors = this.elements.editModal.querySelectorAll('.taxonomies .jvb-selector');
|
|
let submit = Object.fromEntries(formData);
|
taxonomySelectors.forEach(selector => {
|
|
const tax = selector.dataset.taxonomy.replace(jvbSettings.base || 'jvb_', ''); // Remove base prefix
|
delete submit['edit-'+tax];
|
// Check if the TaxonomySelector instance exists
|
if (selector.__instance) {
|
// Get selected terms directly from the selectedItems property
|
const selectedItems = selector.__instance.selectedItems;
|
|
if (selectedItems && Object.keys(selectedItems).length > 0) {
|
// Convert to array of IDs and join with commas
|
taxonomies[tax] = Object.keys(selectedItems).join(',');
|
}
|
}
|
});
|
submit.taxonomies = taxonomies;
|
for(let [key, value] of Object.entries(submit)){
|
if(value === '' || Object.keys(value).length === 0){
|
delete submit[key];
|
}
|
}
|
this.queueContentUpdate(this.elements.editModal.dataset.id, submit);
|
}
|
});
|
}
|
|
|
// Bulk edit modal
|
const bulkEditModal = this.elements.bulkEditModal;
|
if (bulkEditModal) {
|
let hasChanges = false;
|
const form = bulkEditModal.querySelector('form');
|
|
// Track form changes
|
form?.addEventListener('change', () => {
|
hasChanges = true;
|
});
|
|
// Add escape key handler
|
bulkEditModal.addEventListener('keydown', (e) => {
|
if (e.key === 'Escape') {
|
e.preventDefault(); // Prevent default escape behavior
|
this.handleModalClose(bulkEditModal, hasChanges);
|
}
|
});
|
|
// Add backdrop click handler
|
bulkEditModal.addEventListener('click', (e) => {
|
if (e.target === bulkEditModal) {
|
this.handleModalClose(bulkEditModal, hasChanges);
|
}
|
});
|
|
// Handle close button
|
bulkEditModal.querySelector('.cancel')?.addEventListener('click', () => {
|
this.handleModalClose(bulkEditModal, hasChanges);
|
this.clearSelection();
|
});
|
|
// Handle save button
|
bulkEditModal.querySelector('.save')?.addEventListener('click', () => {
|
const formData = new FormData(form);
|
|
|
// Get all selected post IDs from the checkboxes
|
const selectedPosts = Array.from(formData.getAll('posts')); // Get all selected post IDs
|
|
// Format data for queue manager
|
const posts = {};
|
if(formData.get('term_name') === ''){
|
formData.delete('term_name');
|
formData.delete('select_parent');
|
}else{
|
//handle new term creation
|
}
|
|
let taxonomies = {};
|
const taxonomySelectors = bulkEditModal.querySelectorAll('.taxonomies .jvb-selector');
|
|
taxonomySelectors.forEach(selector => {
|
const tax = selector.dataset.taxonomy.replace(jvbSettings.base || 'jvb_', ''); // Remove base prefix
|
// Check if the TaxonomySelector instance exists
|
if (selector.__instance) {
|
// Get selected terms directly from the selectedItems property
|
const selectedItems = selector.__instance.selectedItems;
|
|
if (selectedItems && Object.keys(selectedItems).length > 0) {
|
// Convert to array of IDs and join with commas
|
taxonomies[tax] = Object.keys(selectedItems).join(',');
|
}
|
}
|
});
|
|
selectedPosts.forEach(postID => {
|
posts[postID] = {
|
append: true,
|
content: this.config.content,
|
status: formData.get('bulk_status'), // Get status if changed
|
taxonomies: taxonomies,
|
};
|
});
|
|
// Queue the bulk update operation
|
this.queueManager.addToQueue({
|
type: 'content_update',
|
data: {
|
posts: posts
|
}
|
});
|
|
hasChanges = false;
|
bulkEditModal.close();
|
this.clearSelection();
|
});
|
|
// Handle form submission
|
bulkEditModal.addEventListener('submit', (e) => {
|
const formData = new FormData(form);
|
|
// Get all selected post IDs from the checkboxes
|
const selectedPosts = Array.from(formData.getAll('posts')); // Get all selected post IDs
|
|
// Format data for queue manager
|
const posts = {};
|
if(formData.get('term_name') === ''){
|
formData.delete('term_name');
|
formData.delete('select_parent');
|
}else{
|
//handle new term creation
|
}
|
|
let taxonomies = {};
|
// Add taxonomy data if present
|
for (const tax of this.config.taxonomies) {
|
taxonomies[tax] = formData.getAll(tax);
|
formData.delete(tax);
|
}
|
|
selectedPosts.forEach(postID => {
|
posts[postID] = {
|
append: true,
|
content: this.config.content,
|
status: formData.get('bulk_status'), // Get status if changed
|
taxonomies: taxonomies,
|
};
|
});
|
|
// Queue the bulk update operation
|
this.queueManager.addToQueue({
|
type: 'content_update',
|
data: {
|
posts: posts
|
}
|
});
|
|
hasChanges = false;
|
bulkEditModal.close();
|
this.clearSelection();
|
});
|
}
|
|
// Add methods to open modals
|
this.openEditModal = (item) => {
|
console.log('Openening whatsit');
|
const modal = this.editModal.modal;
|
if (!modal) return;
|
console.log('continuing');
|
|
// Populate form fields
|
let itemID = item.dataset.id;
|
modal.dataset.id = itemID;
|
let fields = JSON.parse(item.dataset.fields);
|
|
let status = item.dataset.status;
|
|
modal.querySelector('input#set-'+status).checked = true;
|
|
for(let field in fields){
|
let value = fields[field];
|
|
if(value){
|
modal.querySelector('[name='+field+']').value = value;
|
|
if(field === 'featured_image'){
|
console.log(item);
|
modal.querySelector('[data-field=featured_image] .image-display').classList.add('has-image');
|
modal.querySelector('[data-field=featured_image] .image-display img').src = item.dataset.img;
|
}
|
}
|
|
|
}
|
if(modal.querySelector('.image')){
|
document.querySelectorAll('.image').forEach(field => {
|
const fieldName = field.dataset.field;
|
const uploadContainer = field.querySelector('.file-upload-container');
|
|
// Initialize BatchFileUploader for this field
|
const uploader = new window.jvbFileUploader(field,{
|
mode: 'direct',
|
content: this.config.content,
|
postID: itemID,
|
fieldName: fieldName,
|
type: 'image_upload',
|
selectors: {
|
dropZone: uploadContainer, // Pass the element directly
|
uploader: field // Pass the field element itself
|
},
|
onSuccess: (result) => this.handleImageUploadSuccess(result, field),
|
onError: (error) => this.handleImageUploadError(error, field)
|
});
|
|
// Handle remove button
|
const removeButton = field.querySelector('.remove-image');
|
if (removeButton) {
|
removeButton.addEventListener('click', () => {
|
this.handleImageRemove(field);
|
});
|
}
|
|
// Handle replace button
|
const replaceButton = field.querySelector('.replace-image');
|
if (replaceButton) {
|
replaceButton.addEventListener('click', () => {
|
const fileInput = field.querySelector('input[type="file"]');
|
fileInput.click();
|
});
|
}
|
});
|
}
|
if(modal.querySelector('.gallery')){
|
document.querySelectorAll('.gallery').forEach(field => {
|
const fieldName = field.dataset.field;
|
const previewGrid = field.querySelector('.gallery-preview');
|
|
if(item.dataset.images){
|
let urls = item.dataset.images.split(',');
|
urls.forEach(url=>{
|
this.addToGalleryPreview(url,previewGrid);
|
});
|
}
|
|
// Initialize BatchFileUploader
|
const uploader = new window.jvbFileUploader(field, {
|
mode: 'gallery',
|
selectors: {
|
dropZone: field.querySelector('.file-upload-container'),
|
previewGrid: previewGrid,
|
uploader: field // Pass the field element itself
|
},
|
type: 'image_upload',
|
content: this.config.content,
|
postID: itemID,
|
fieldName: fieldName,
|
onUploadComplete: (result) => {
|
// Update hidden input with new IDs
|
const hiddenInput = field.querySelector('input[type="hidden"]');
|
const currentIds = hiddenInput.value ? hiddenInput.value.split(',') : [];
|
const newIds = result.data.map(file => file.attachment_id);
|
hiddenInput.value = [...currentIds, ...newIds].join(',');
|
|
// Add new preview items
|
result.data.forEach(file => {
|
const preview = document.createElement('div');
|
preview.className = 'preview-item';
|
preview.dataset.id = file.attachment_id;
|
preview.draggable = true;
|
preview.innerHTML = `
|
<img src="${file.url}" alt="Upload preview">
|
<button type="button" class="remove-preview">
|
${jvbSettings.icons.delete}
|
</button>
|
<button type="button" class="move-image">
|
${jvbSettings.icons.grab}
|
</button>
|
`;
|
previewGrid.appendChild(preview);
|
});
|
|
// Trigger change event
|
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
|
}
|
});
|
|
// Initialize Sortable
|
new Sortable(previewGrid, {
|
animation: 150,
|
handle: '.move-image', // Add a move handle for better UX
|
onEnd: () => {
|
// Update hidden input with new order
|
const hiddenInput = field.querySelector('input[type="hidden"]');
|
const ids = [...previewGrid.querySelectorAll('.preview-item')]
|
.map(item => item.dataset.id);
|
hiddenInput.value = ids.join(',');
|
|
// Trigger change event
|
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
|
}
|
});
|
});
|
}
|
|
if(modal.querySelector('.taxonomies')){
|
let taxonomies = modal.querySelectorAll('.taxonomies .jvb-selector');
|
taxonomies.forEach(taxonomy => {
|
let tax = taxonomy.dataset.taxonomy;
|
let hierarchical = taxonomy.classList.contains('hierarchical');
|
let config = JSON.parse(taxonomy.dataset.config);
|
|
let selected = item.dataset[tax] ? JSON.parse(item.dataset[tax]) : {};
|
|
let terms = config.common;
|
taxonomy.__instance = new window.jvbSelector(taxonomy, {
|
title: 'Select '+tax+'(s)',
|
selected: selected,
|
common: terms,
|
allowMultiple: config.multiple,
|
createNew: true,
|
});
|
|
|
});
|
}
|
|
modal.showModal();
|
};
|
|
this.openBulkEditModal = () => {
|
const modal = this.elements.bulkEditModal;
|
if (!modal) return;
|
const items = this.state.selected;
|
|
// Update selected count
|
const count = modal.querySelector('.selected-count');
|
if (count) count.textContent = `(${items.length} items)`;
|
|
const taxonomySelectors = bulkEditModal.querySelectorAll('.taxonomies .jvb-selector');
|
taxonomySelectors.forEach(selector => {
|
const tax = selector.dataset.taxonomy;
|
const hierarchical = selector.classList.contains('hierarchical');
|
const config = JSON.parse(selector.dataset.config);// Initialize with empty selections and append mode
|
selector.__instance = new window.jvbSelector(selector, {
|
title: `Select ${tax}(s)`,
|
values: {}, // Start with empty selections
|
allowMultiple: config.multiple,
|
appendMode: true, // Add this flag for the saving behavior
|
createNew: true,
|
});
|
});
|
|
modal.showModal();
|
};
|
}
|
|
// Helper method to handle modal closing with unsaved changes
|
handleModalClose(modal, hasChanges) {
|
if (hasChanges) {
|
if (confirm('You have unsaved changes. Are you sure you want to close this window?')) {
|
// Clean up any BatchFileUploader instances
|
modal.querySelectorAll('.gallery').forEach(field => {
|
if (field.__uploader) {
|
field.__uploader.cleanup();
|
delete field.__uploader;
|
}
|
});
|
modal.close();
|
return true;
|
}
|
return false;
|
}
|
modal.close();
|
return true;
|
}
|
|
addToGalleryPreview(url, grid) {
|
const preview = document.createElement('div');
|
preview.className = 'preview-item'; // Add uploading state
|
preview.draggable = true;
|
preview.innerHTML = `
|
<img src="${url}" alt="Upload preview">
|
<div class="upload-status">
|
<div class="upload-progress"></div>
|
</div>
|
<button type="button" class="remove-preview" title="Remove Image">
|
${jvbSettings.icons.delete}
|
</button>
|
<button type="button" class="move-image" title="Reorder Image">
|
${jvbSettings.icons.grab}
|
</button>
|
`;
|
|
|
grid.appendChild(preview);
|
return preview;
|
}
|
|
handleImageUploadSuccess(result, field) {
|
if (!result.data || !result.data.length) return;
|
|
const imageDisplay = field.querySelector('.image-display');
|
removeChildren(imageDisplay)
|
imageDisplay.classList.add('has-image');
|
let ids = [];
|
result.data.forEach(file =>{
|
let img = new Image();
|
img.src = file.url;
|
ids.push(file['attachment_id']);
|
imageDisplay.appendChild(img);
|
});
|
|
const hiddenInput = field.querySelector('input[type="hidden"]');
|
hiddenInput.value = ids.join(',');
|
const uploadContainer = field.querySelector('.file-upload-container');
|
uploadContainer.hidden = true;
|
|
// Show success notification
|
this.showNotification('Image updated successfully');
|
}
|
|
handleImageUploadError(error, field) {
|
console.error('Upload error:', error);
|
this.showNotification('Failed to upload image','error');
|
|
// Reset field if needed
|
const uploadContainer = field.querySelector('.file-upload-container');
|
uploadContainer.hidden = false;
|
|
// Clear any error states
|
const errorElement = field.querySelector('.file-error');
|
if (errorElement) {
|
errorElement.textContent = '';
|
}
|
}
|
|
handleImageRemove(field) {
|
const imageDisplay = field.querySelector('.image-display');
|
const img = imageDisplay.querySelector('img');
|
const hiddenInput = field.querySelector('input[type="hidden"]');
|
const uploadContainer = field.querySelector('.file-upload-container');
|
|
// Clear the hidden input
|
hiddenInput.value = '';
|
|
// Reset UI
|
img.src = '';
|
imageDisplay.classList.remove('has-image');
|
uploadContainer.hidden = false;
|
|
// Show notification
|
this.showNotification('Image removed');
|
}
|
|
|
clearSelection() {
|
const items = this.getVisibleItems();
|
items.forEach(item => this.toggleItemSelection(item, false));
|
this.state.selected.clear();
|
this.selectAll.checked = false;
|
this.updateBulkControls();
|
}
|
|
updateBulkControls() {
|
const hasSelection = this.state.selected.size > 0;
|
this.elements.grid.classList.toggle('selecting', hasSelection);
|
this.elements.bulkControls.classList.toggle('has-selection', hasSelection);
|
this.elements.bulkControls.querySelector('.bulk-actions').hidden = !hasSelection;
|
|
if(hasSelection){
|
document.addEventListener('keydown', (e) => {
|
if (e.key === 'Escape' && this.state.selected.size > 0) {
|
// Only handle escape if we have selections
|
this.clearSelection();
|
this.showNotification('Selection cleared');
|
}
|
});
|
}
|
const count = this.elements.bulkControls.querySelector('.selected-count');
|
if (count) {
|
count.textContent = hasSelection ? `( ${this.state.selected.size} selected )` : '';
|
}
|
}
|
|
|
|
|
// Utility Methods
|
getVisibleItems() {
|
return Array.from(this.elements.grid.querySelectorAll('.item:not([hidden])'));
|
}
|
|
showNotification(message, type = 'success') {
|
if (window.jvbNotifications) {
|
window.jvbNotifications.showPopupNotification({
|
message,
|
type,
|
priority: 'medium',
|
duration: 3000
|
});
|
} else {
|
alert(message);
|
}
|
}
|
}
|
|
window.contentManager = ContentManager;
|