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 = ` //
// // //
// ${item.alt} // ${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 = ` //
// // //
// ${item.alt} // ${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 = ` Upload preview `; 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 = ` Upload preview
`; 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;