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(),
'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(),
// '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 = `
${jvbSettings.icons[this.config.content]}Nothing here${jvbSettings.icons[this.config.content]}
It doesn't look like you have any ${this.config.plural} yet.
Add some by uploading images above.
`;
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 = `
//
//
//
//
//
// ${this.render_item_actions(item)}
//
// `;
//
// html += `
// ${dashboardSettings.icons.calendar}Date published
// ${item.date}
`;
// 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 += `
// ${jvbSettings.icons[tax.icon]}${tax.name}`;
// terms.forEach(term => {
// html += `${term[1]}`;
// });
// html += `
`;
// }
// };
// html += `
`;
// return html;
// break;
// }
// }
//
// render_list_item(item){
// let html;
// switch(this.config.content){
// case 'tattoo':
// case 'artwork':
// html = `
//
//
//
//
//
// ${this.render_item_actions(item)}
// `;
// html += `
// ${dashboardSettings.icons.calendar}Date published
// ${item.date}
`;
// 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 += `
// ${tax.icon}${tax.name}`;
// terms.forEach(term => {
// html += `${term[1]}`;
// });
// html += `
`;
// }
// };
// html += `
`;
// html += ``;
// 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 += '';
});
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 = `
`;
} else {
bulkActionSelect.innerHTML = `
`;
}
}
// 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 = `
`;
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 = `
`;
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;