/**
|
* The base CRUD manager: helps reduce code duplication between managing different content types
|
*/
|
|
class CRUD {
|
/**
|
* @param config
|
*/
|
constructor(config) {
|
console.log('Initializing Crud.js');
|
this.config = {
|
content: '',
|
type: 'post',
|
|
api: 'content',
|
batchSize: 10,
|
upload: {
|
mode: 'direct',
|
allowMultiple: true,
|
allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
|
},
|
selectors: {
|
container: '.replace',
|
gridWrap: '.items-list',
|
grid: '.items-list .item-grid',
|
view: '.radio-options.view',
|
columnsSelect: 'details.multi-select',
|
bulk: '.bulk-controls',
|
bulkActions: '.bulk-action-select',
|
selectAll: 'input.select-all',
|
count: '.bulk-controls .selected-count',
|
search: 'input[type="search"]',
|
scroll: '.scroll-sentinel',
|
add: '.create-item',
|
dateRange: 'dialog.date-range',
|
filters: '.items-list .all-filters',
|
clearButton: '.clear-filters',
|
uploader: 'details.uploader'
|
},
|
filters: {},
|
modals: {},
|
tabs: {},
|
...config
|
};
|
|
|
this.editing = false;
|
|
this.api = this.config.api;
|
this.content = this.config.content;
|
|
this.plural = jvbSettings.labels[this.content.replace('-', '_')].plural ?? this.content + 's';
|
this.single = jvbSettings.labels[this.content.replace('-', '_')].single ?? this.content;
|
|
this.batchSize = this.config.batchSize; // for renderItems document fragment
|
this.tabs = window.isEmptyObject(this.config.tabs) ? false : this.config.tabs;
|
this.tabNav = false;
|
this.editModal = document.querySelector('dialog.edit-modal').firstElementChild.cloneNode(true).cloneNode(true);
|
this.bulkEditModal = document.querySelector('dialog.bulk-edit-modal').firstElementChild.cloneNode(true).cloneNode(true);
|
this.createModal = document.querySelector('dialog.create-modal').firstElementChild.cloneNode(true).cloneNode(true);
|
|
this.config.modals = {
|
create: {
|
open: '.create-item',
|
selector: '.create-modal',
|
openMessage: 'Opened Create New ' + this.single + ' Modal',
|
closeMessage: 'Closed Create New ' + this.single + ' Modal',
|
onOpen: () => this.openCreateModal(),
|
onRender: () => this.renderCreateModal(),
|
onClose: () => this.closeCreateModal(),
|
onSave: (changes) => this.handleCreateModalSave(changes),
|
},
|
edit: {
|
selector: '.edit-modal',
|
open: 'button[data-action="edit"]',
|
openMessage: 'Opened Edit ' + this.single + ' Modal',
|
closeMessage: 'Closed Edit ' + this.single + ' Modal',
|
// onOpen: (e, modal) => this.openEditModal(e, modal),
|
onRender: (e, modal) => this.renderEditModal(e, modal),
|
onClose: (e, changes) => this.closeEditModal(e, changes),
|
onSave: (changes) => this.handleEditModalSave(changes),
|
},
|
bulkEdit: {
|
selector: '.bulk-edit-modal',
|
openMessage: 'Opened Bulk Edit ' + this.plural + ' Modal',
|
closeMessage: 'Closed Bulk Edit ' + this.plural + ' Modal',
|
onOpen: (e, modal) => this.openBulkEditModal(e, modal),
|
onRender: () => this.renderBulkEditModal(),
|
onClose: (e, changes) => this.closeBulkEditModal(e, changes),
|
onSave: (changes) => this.handleBulkEditModalSave(changes),
|
},
|
dateRange: {
|
selector: '.date-range',
|
openMessage: 'Opened Date Range selection',
|
closeMessage: 'Closed Date Range selection',
|
onOpen: () => this.handleDateRangeOpen(),
|
onClose: () => this.handleDateRangeClose(),
|
}
|
,
|
...this.config.modals
|
};
|
|
//Core components
|
this.a11y = window.jvbA11y;
|
this.errors = window.jvbError;
|
this.cache = window.jvbCache;
|
this.queue = window.jvbQueue;
|
this.loading = window.jvbLoading;
|
|
window.jvbLoading.setContent(this.content);
|
|
//Filters Management
|
this.filters = {};
|
this.resetFilters();
|
this.isLoading = false;
|
this.hasMore = true;
|
this.totalItems = null;
|
this.pages = 1;
|
|
//View Management
|
this.view = null;
|
this.views = new Map();
|
this.viewSettings = {};
|
|
//Stores fetched posts
|
this.posts = new Map();
|
//Stores selected items in a map
|
this.selected = new Set();
|
|
this.initElements();
|
this.initListeners();
|
|
this.table = null;
|
this.isTable = false;
|
|
this.initView();
|
|
this.handleEscape = (e) => {
|
if (e.key === 'Escape' && this.selected.size > 0) {
|
this.clearSelection();
|
this.elements.selectAll.nextElementSibling.firstElementChild.innerText = 'Select All';
|
this.a11y.announce('Selection cleared');
|
}
|
}
|
|
this.lastSelectedItem = null; // Track last selected item for range selection
|
this.selectionMode = false;
|
|
}
|
|
async initView() {
|
this.views.set('grid', {
|
name: 'grid',
|
type: 'visual',
|
template: 'gridView',
|
containerClass: 'grid-view',
|
init: () => this.initVisualView('grid'),
|
activate: () => this.activateVisualView('grid'),
|
deactivate: () => this.deactivateVisualView('grid'),
|
render: (items, append) => this.renderVisualItems(items, 'grid', append)
|
});
|
|
this.views.set('list', {
|
name: 'list',
|
type: 'visual',
|
template: 'listView',
|
containerClass: 'list-view',
|
init: () => this.initVisualView('list'),
|
activate: () => this.activateVisualView('list'),
|
deactivate: () => this.deactivateVisualView('list'),
|
render: (items, append) => this.renderVisualItems(items, 'list', append)
|
});
|
|
this.views.set('table', {
|
name: 'table',
|
type: 'editable',
|
template: 'tableView',
|
containerClass: 'table-view',
|
tableContainer: null,
|
tableForm: null,
|
init: () => this.initTableView(),
|
activate: () => this.activateTableView(),
|
deactivate: () => this.deactivateTableView(),
|
render: (items, append) => this.renderTableItems(items, append),
|
cleanup: () => this.cleanupTableView()
|
});
|
let view = await this.loadViewSettings();
|
await this.switchView(view);
|
await this.loadContent(true);
|
}
|
|
clearSelection() {
|
if (this.selected.size === 0) {
|
return;
|
}
|
|
this.selected.clear();
|
|
let checkboxes;
|
if (this.isTable) {
|
checkboxes = document.querySelectorAll('table .select-checkbox:checked');
|
} else {
|
checkboxes = this.elements.grid.querySelectorAll('.select-checkbox:checked');
|
}
|
checkboxes.forEach((check) => {
|
check.checked = false;
|
});
|
this.elements.selectAll.checked = false;
|
this.lastSelectedItem = null; // Reset last selected item
|
this.selectionMode = false; // Exit selection mode
|
this.updateBulkControls();
|
}
|
|
initElements() {
|
this.elements = {};
|
|
// Cache all configured selectors
|
Object.entries(this.config.selectors).forEach(([key, selector]) => {
|
if (typeof selector === 'object') {
|
this.elements[key] = {};
|
Object.entries(selector).forEach(([subKey, subSelector]) => {
|
this.elements[key][subKey] = document.querySelector(subSelector);
|
});
|
} else {
|
this.elements[key] = document.querySelector(selector);
|
}
|
});
|
|
// Ensure required elements exist
|
if (!this.elements.container) {
|
throw new Error(`CRUD Manager: Container element not found for ${this.config.content}`);
|
}
|
|
if (!this.elements.grid) {
|
throw new Error(`CRUD Manager: Grid element not found for ${this.config.content}`);
|
}
|
}
|
|
initListeners() {
|
this.clickHandler = this.handleClick.bind(this);
|
this.changeHandler = this.handleChange.bind(this);
|
this.keyHandler = this.handleKeydown.bind(this);
|
document.addEventListener('click', this.clickHandler);
|
document.addEventListener('change', this.changeHandler);
|
document.addEventListener('keydown', this.keyHandler);
|
this.initInfiniteScroll();
|
this.initUploader();
|
this.initModals();
|
this.initTabs();
|
this.initFilters();
|
}
|
|
handleClick(e) {
|
if (window.targetCheck(e, '.item-select') && e.shiftKey) {
|
e.preventDefault();
|
this.handleRangeSelection(e.target);
|
} else if (window.targetCheck(e, '.item-select')) {
|
const checkbox = e.target.closest('.item-select').querySelector('.select-checkbox');
|
if (checkbox) {
|
this.lastSelectedItem = checkbox.closest('.item');
|
this.selectionMode = this.selected.size > 0;
|
}
|
} else if (window.targetCheck(e, '.action') && window.targetCheck(e, '.item')) {
|
|
let action = window.targetCheck(e, '.action');
|
let item = window.targetCheck(e, '.item');
|
this.handleItemAction(action.dataset.action, item);
|
} else if (this.isTable && window.targetCheck(e, '.create-item')) {
|
this.addEmptyRow();
|
} else if (window.targetCheck(e, '.apply-bulk')) {
|
this.handleBulkControl();
|
} else if (window.targetCheck(e, '.cancel-bulk')) {
|
this.clearSelection();
|
} else if (window.targetCheck(e, this.config.selectors.clearButton)) {
|
this.handleClearFilters();
|
} else if (window.targetCheck(e, 'details.uploader summary')) {
|
this.saveViewSettings();
|
}
|
|
}
|
|
handleRangeSelection(target) {
|
if (!this.lastSelectedItem) {
|
// If no last selected item, treat as normal selection
|
this.updateSelected(target.closest('.item-select').querySelector('.select-checkbox'));
|
return;
|
}
|
|
if (this.isTable) {
|
|
}
|
let container = (this.isTable) ? document.querySelector('table') : this.elements.grid;
|
|
const currentItem = target.closest('.item');
|
const allItems = Array.from(container.querySelectorAll('.item'));
|
|
const lastIndex = allItems.indexOf(this.lastSelectedItem);
|
const currentIndex = allItems.indexOf(currentItem);
|
|
if (lastIndex === -1 || currentIndex === -1) {
|
// Fallback to normal selection if items not found
|
this.updateSelected(target.closest('.item-select').querySelector('.select-checkbox'));
|
return;
|
}
|
|
// Determine range
|
const startIndex = Math.min(lastIndex, currentIndex);
|
const endIndex = Math.max(lastIndex, currentIndex);
|
|
// Select all items in range
|
let newSelections = 0;
|
for (let i = startIndex; i <= endIndex; i++) {
|
const item = allItems[i];
|
const checkbox = item.querySelector('.select-checkbox');
|
|
if (checkbox && !checkbox.checked) {
|
checkbox.checked = true;
|
this.selected.add(checkbox.value);
|
newSelections++;
|
}
|
}
|
|
this.updateBulkControls();
|
this.selectionMode = this.selected.size > 0;
|
|
// Announce to screen readers
|
if (window.jvbA11y) {
|
const rangeSize = endIndex - startIndex + 1;
|
window.jvbA11y.announce(`Selected range of ${rangeSize} items. ${newSelections} new selections. ${this.selected.size} total selected.`);
|
}
|
}
|
|
handleKeydown(e) {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
|
e.preventDefault();
|
this.elements.selectAll.checked = true;
|
this.selectAll();
|
}
|
}
|
|
handleChange(e) {
|
|
if (this.isTable && window.targetCheck(e, 'input#vertical')) {
|
e.preventDefault();
|
this.viewSettings.tabNav = e.target.checked;
|
// Save preference
|
localStorage.setItem('jvbTabNav', e.target.checked ? 'vertical' : 'horizontal');
|
|
// Announce change
|
window.jvbA11y.announce(
|
this.viewSettings.tabNav ? 'Changed to vertical navigation' : 'Changed to horizontal navigation'
|
);
|
|
// Save view settings
|
this.saveViewSettings();
|
return;
|
} else if (this.isTable && window.targetCheck(e, '.multi-select')) {
|
this.handleColumnVisibility(e);
|
}
|
if (window.targetCheck('select.date-filter')) {
|
let value = e.target.value;
|
if (value !== 'custom') {
|
if (this.elements.dateRange.open) {
|
this.modals.dateRange.handleClose();
|
}
|
|
const monthSelect = this.elements.dateRange.querySelector('.month-select');
|
if (monthSelect) {
|
monthSelect.value = '';
|
}
|
this.setDateFilter(value);
|
|
}
|
}
|
switch (true) {
|
case e.target.value === 'custom':
|
this.openModal('dateRange');
|
break;
|
case 'action' in e.target.dataset:
|
|
break;
|
case 'taxonomy' in e.target.dataset:
|
this.updateFilters(e.target.dataset.filter, e.target.value, e.target.dataset.taxonomy);
|
break;
|
case 'filter' in e.target.dataset:
|
this.updateFilters(e.target.dataset.filter, e.target.value);
|
break;
|
case 'view' in e.target.dataset:
|
this.switchView(e.target.dataset.view);
|
break;
|
}
|
if (window.targetCheck(e, '.item-select')) {
|
this.updateSelected(e.target);
|
} else if (window.targetCheck(e, this.config.selectors.selectAll)) {
|
this.selectAll();
|
} else if (window.targetCheck(e, '.date-range .month-select')) {
|
this.handleMonthSelect(e);
|
} else if (window.targetCheck(e, '.date-range .date-start') || window.targetCheck(e, '.date-range .date-end')) {
|
let start = e.target.closest('.date-range').querySelector('.date-start').value;
|
let end = e.target.closest('.date-range').querySelector('.date-end').value;
|
|
if (start && end) {
|
let startDate = new Date(start);
|
let endDate = new Date(end);
|
endDate.setHours(23, 59, 59, 999);
|
this.setDateFilter('custom', startDate, endDate);
|
this.modals.dateRange.handleClose();
|
}
|
}
|
}
|
|
|
selectAll() {
|
if (this.elements.selectAll.checked) {
|
this.elements.selectAll.nextElementSibling.firstElementChild.innerText = 'Clear Selection';
|
let container = (this.isTable) ? document.querySelector('table') : this.elements.grid;
|
container.querySelectorAll('.select-checkbox:not(:checked)').forEach((check) => {
|
check.checked = true;
|
this.selected.add(check.value);
|
});
|
this.updateBulkControls();
|
} else {
|
this.elements.selectAll.nextElementSibling.firstElementChild.innerText = 'Select All';
|
this.clearSelection();
|
}
|
|
}
|
|
openModal(modal) {
|
if (this.modals[modal]) {
|
this.modals[modal].handleOpen();
|
}
|
}
|
|
initInfiniteScroll() {
|
if (!this.elements.scroll) return;
|
const observer = new IntersectionObserver(entries => {
|
entries.forEach(entry => {
|
if (entry.isIntersecting && this.hasMore) {
|
this.loadContent();
|
}
|
})
|
});
|
|
observer.observe(this.elements.scroll);
|
}
|
|
initModals() {
|
this.modals = {};
|
for (let [modal, config] of Object.entries(this.config.modals)) {
|
let m = document.querySelector(config.selector);
|
if (m) {
|
this.modals[modal] = new window.jvbModal(m, config);
|
}
|
}
|
}
|
|
initUploader() {
|
const uploader = document.querySelector('details.uploader .field.image');
|
if (!uploader) {
|
return;
|
}
|
|
// Register with centralized UploadManager instead of creating new instance
|
// Store reference for later use
|
this.uploaderFieldId = window.jvbUploadManager.registerUploader(uploader, {
|
content: this.content,
|
onUploadComplete: (result) => this.handleUploadComplete(result),
|
onGroupingComplete: (result) => this.loadContent(true)
|
});
|
|
}
|
|
handleUploadComplete(result) {
|
|
if (result.success && result.data) {
|
location.reload();
|
}
|
}
|
|
showPreview(e) {
|
console.log(e);
|
}
|
|
initTabs() {
|
}
|
|
initFilters() {
|
}
|
|
createContent() {
|
}
|
|
editContent() {
|
}
|
|
bulkEditContent() {
|
}
|
|
updateSelected(element) {
|
if (element.checked) {
|
this.selected.add(element.value);
|
this.lastSelectedItem = element.closest('.item'); // Update last selected
|
} else if (this.selected.has(element.value)) {
|
this.selected.delete(element.value);
|
this.lastSelectedItem = null;
|
}
|
|
this.selectionMode = this.selected.size > 0;
|
this.updateBulkControls();
|
}
|
|
updateBulkControls() {
|
const selecting = this.selected.size > 0;
|
this.elements.grid.classList.toggle('selecting', selecting);
|
this.elements.bulk.querySelector('.bulk-actions').hidden = !selecting;
|
|
if (selecting) {
|
document.addEventListener('keydown', this.handleEscape);
|
} else {
|
document.removeEventListener('keydown', this.handleEscape);
|
}
|
|
if (this.elements.count) {
|
this.elements.count.textContent = selecting ? `( ${this.selected.size} selected )` : '';
|
}
|
|
window.removeChildren(this.elements.bulkActions);
|
let options;
|
if (this.filters.status === 'trash') {
|
options = window.getTemplate('trashOptions');
|
} else {
|
options = window.getTemplate('notTrashOptions');
|
}
|
options.querySelectorAll('option').forEach((option) => {
|
this.elements.bulkActions.append(option);
|
});
|
this.elements.bulkActions.firstElementChild.checked = true;
|
this.elements.bulkActions.firstElementChild.selected = true;
|
options.remove();
|
|
}
|
|
handleBulkControl() {
|
let action = this.elements.bulkActions.value;
|
switch (action) {
|
case 'delete':
|
if (confirm(`Hold up! Are you sure you want to permanently delete these ${this.selected.size} ${this.plural}?\n\nThis is a forever kind of deal - no takebacks!`)) {
|
this.handleBulkEdit('delete');
|
}
|
break;
|
case 'edit':
|
this.modals['bulkEdit'].handleOpen();
|
break;
|
case 'restore':
|
this.handleBulkEdit('draft');
|
break;
|
case 'trash':
|
case 'publish':
|
case 'draft':
|
this.handleBulkEdit(action);
|
break;
|
|
}
|
}
|
|
handleItemAction(action, item) {
|
const ID = item.dataset.id;
|
this.clearSelection();
|
switch (action) {
|
case 'restore':
|
case 'trash':
|
this.selected.add(ID);
|
this.handleBulkEdit(action);
|
break;
|
case 'delete':
|
if (confirm(`Hold up! Are you sure you want to permanently delete this ${this.single}?\n\nThis is a forever kind of deal - no taking it back.`)) {
|
this.selected.add(ID);
|
this.handleBulkEdit(action);
|
}
|
break;
|
case 'toggle-status':
|
const current = item.dataset.status;
|
const newStatus = current === 'publish' ? 'draft' : 'publish';
|
this.selected.add(ID);
|
this.handleBulkEdit(newStatus);
|
item.dataset.status = newStatus;
|
window.removeChildren(item.querySelector('[data-action="toggle-status"]'));
|
item.querySelector('[data-action="toggle-status"]').append(window.getIcon(newStatus));
|
break;
|
|
}
|
}
|
|
resetFilters(elements = false) {
|
this.filters = {
|
content: this.content,
|
status: 'all',
|
taxonomies: {},
|
page: 1,
|
order: 'DESC',
|
orderby: 'date',
|
...this.config.filters //additional filters can be added in constructor
|
}
|
|
|
|
if (elements) {
|
let checks = [this.filters.status, this.filters.order, this.filters.orderby];
|
checks.forEach((check) => {
|
let item = this.elements.filters.querySelector(`[data-filter][value="${check}"]`);
|
if (item) {
|
item.checked = true;
|
}
|
});
|
|
this.elements.filters.querySelectorAll('select').forEach(select => {
|
select.value = '';
|
});
|
|
this.updateClearFiltersButton();
|
this.hasMore = true;
|
this.loadContent(true);
|
}
|
|
}
|
|
async switchView(view) {
|
if (!this.views.has(view)) {
|
console.error(`View "${view}" not registered`);
|
return;
|
}
|
|
// Don't switch if already in this view
|
if (this.view === view) {
|
return;
|
}
|
|
try {
|
// Store current data if we have any
|
const hasData = this.posts.size > 0;
|
|
// Deactivate current view
|
if (this.view) {
|
const currentViewObj = this.views.get(this.view);
|
await currentViewObj.deactivate();
|
}
|
|
// Activate new view
|
const newView = this.views.get(view);
|
await newView.activate();
|
|
this.view = view;
|
this.elements.view.querySelector(`[data-view="${view}"]`).checked = true;
|
|
// Save view preference
|
this.saveViewSettings();
|
|
// If we already have data, just re-render it in the new view
|
if (hasData) {
|
// Convert Map to array for rendering
|
const items = Array.from(this.posts.values());
|
|
// Clear the display area first
|
this.clearContent();
|
|
// Render existing data in new view format
|
this.renderItems(items, false);
|
|
// Show loading state briefly for UX
|
window.jvbA11y?.announce(`Switched to ${view} view`);
|
} else {
|
// Only load data if we don't have any
|
this.hasMore = true;
|
this.loadContent(true);
|
}
|
|
} catch (error) {
|
console.error('Error switching view:', error);
|
window.showToast?.(`Failed to switch to ${view} view`, 'error');
|
}
|
}
|
|
|
/**
|
* Updates the filters from the node element
|
* HTML element MUST have: data-filter-by and data-filter
|
* @param {string} filter
|
* @param {string} value
|
* @param {?string} taxonomy
|
*/
|
updateFilters(filter, value, taxonomy = null) {
|
|
// If the filter isn't defined in the filter object, we won't be able to filter by it
|
if (Object.hasOwn(this.filters, filter)) {
|
if (taxonomy !== null) {
|
if (!Object.hasOwn(this.filters[filter], taxonomy)) {
|
this.filters[filter][taxonomy] = [];
|
}
|
this.filters[filter][taxonomy].push(value);
|
} else {
|
this.filters[filter] = value;
|
}
|
|
this.saveViewSettings();
|
this.updateClearFiltersButton();
|
}
|
this.hasMore = true;
|
this.loadContent(true);
|
}
|
|
buildFilters() {
|
const temp = {};
|
for (const [name, value] of Object.entries(this.filters)) {
|
if (value) {
|
if (typeof value === 'object' && window.isEmptyObject(value)) {
|
|
} else if (typeof value === 'object') {
|
temp[name] = {};
|
for (let [key, v] of Object.entries(value)) {
|
temp[name][key] = v.join(',');
|
}
|
|
temp[name] = JSON.stringify(temp[name]);
|
} else {
|
temp[name] = value;
|
}
|
}
|
}
|
temp.user = jvbSettings.currentUser;
|
|
return new URLSearchParams(temp);
|
}
|
|
async loadContent(reset = false, force = false) {
|
if (this.isLoading || !this.hasMore) return;
|
|
try {
|
this.isLoading = true;
|
this.loading.showLoading();
|
if (reset) {
|
this.filters.page = 1;
|
this.clearContent();
|
}
|
|
const filters = this.buildFilters();
|
const data = await this.cache.fetchWithCache(
|
`${jvbSettings.api}${this.api}?${filters.toString()}`,
|
{
|
method: 'GET',
|
headers: {
|
'X-WP-Nonce': jvbSettings.nonce,
|
'action_nonce': jvbSettings.dash,
|
}
|
},
|
{
|
context: this.content,
|
forceRefresh: force,
|
}
|
);
|
|
console.log('Fetched data: ', data);
|
|
if (data.pagination) {
|
this.hasMore = data['has_more'];
|
this.totalItems = data.items;
|
this.pages = data.pages;
|
} else {
|
this.hasMore = false;
|
this.totalItems = 0;
|
this.pages = 0;
|
}
|
|
if (data.items && data.items.length > 0) {
|
this.renderItems(data.items, this.filters.page >1);
|
this.cacheItems(data.items);
|
} else if (reset) {
|
this.showEmptyState();
|
}
|
|
if (this.hasMore) {
|
this.filters.page++;
|
}
|
} catch (error) {
|
this.handleError(
|
error,
|
'loadContent'
|
);
|
throw error;
|
} finally {
|
this.isLoading = false;
|
this.loading.hideLoading();
|
}
|
}
|
|
clearContent() {
|
if (this.view === 'table') {
|
const tbody = document.querySelector('form.table tbody');
|
if (tbody) {
|
window.removeChildren(tbody);
|
}
|
} else {
|
const grid = document.querySelector(this.config.selectors.grid);
|
if (grid) {
|
window.removeChildren(grid);
|
}
|
}
|
}
|
|
/**
|
* Cache items for later reference
|
*/
|
cacheItems(items) {
|
items.forEach(item => {
|
this.posts.set(item.id, item);
|
});
|
}
|
|
showEmptyState() {
|
|
if (this.view === 'table') {
|
const template = window.getTemplate('emptyState');
|
const table = document.querySelector('form.table tbody');
|
if (table) {
|
table.appendChild(template);
|
}
|
} else {
|
const template = window.getTemplate('emptyState');
|
const grid = document.querySelector(this.config.selectors.grid);
|
if (grid) {
|
grid.appendChild(template);
|
}
|
}
|
}
|
|
hideEmptyState() {
|
if (this.isTable) {
|
this.table.classList.remove('empty');
|
} else {
|
this.elements.grid.classList.remove('empty');
|
}
|
|
this.elements.container.querySelector('.empty-state')?.remove();
|
}
|
|
/**
|
* Handle errors
|
* @param {Error} error - Error object
|
* @param {string} action - Action being performed when error occurred
|
*/
|
handleError(error, action) {
|
console.error(`CRUD error (${action}):`, error);
|
|
// Show toast notification
|
showToast(
|
`Error ${action}: ${error.message || 'Something went wrong'}`,
|
'error'
|
);
|
|
// Log with error handler if available
|
if (window.jvbError) {
|
window.jvbError.log(error, {
|
component: 'CRUD',
|
action: action
|
});
|
}
|
|
// Announce to screen readers
|
if (window.jvbA11y) {
|
window.jvbA11y.announce(`Error ${action}. ${error.message || 'Please try again.'}`);
|
}
|
}
|
|
/**
|
* Modal Handlers
|
*/
|
renderEditModal(e, modal) {
|
let item = e.target.closest('.item');
|
this.editing = item.dataset.id;
|
let fields = JSON.parse(item.dataset.fields);
|
let images = JSON.parse(item.dataset.images);
|
|
modal.querySelector('h2').textContent = (fields['post_title'] !== '') ? `Editing "${fields['post_title']}"` : `Editing ${this.single}`;
|
|
if (item.dataset.status) {
|
modal.querySelector(`[name="status"][value="${item.dataset.status}"]`).checked = true;
|
}
|
|
window.jvbForm.removeChangeListener();
|
window.jvbForm.populateFormFields(modal.querySelector('form'), fields, images);
|
// window.jvbForm.processChanges(modal.form, {processSave: false});
|
window.jvbForm.addChangeListener();
|
}
|
|
closeEditModal() {
|
let modal = document.querySelector('dialog.edit-modal');
|
window.removeChildren(modal);
|
let inside = this.editModal.cloneNode(true);
|
modal.append(inside);
|
}
|
|
handleEditModalSave(changes) {
|
let data = {};
|
data[this.editing] = changes;
|
|
this.saveData(data);
|
}
|
|
openBulkEditModal(e, modal) {
|
let selected = modal.querySelector('.selected');
|
for (let item of this.selected) {
|
item = parseInt(item);
|
if (!this.posts.has(item)) {
|
let element = document.querySelector(`.item-grid .item[data-id="${item}"]`);
|
item = {
|
fields: JSON.parse(element.dataset.fields),
|
images: JSON.parse(element.dataset.images)
|
}
|
|
} else {
|
item = this.posts.get(item);
|
}
|
|
let img = window.getTemplate('bulkItem');
|
let check = img.querySelector('input[type=checkbox]');
|
let image = img.querySelector('img');
|
[
|
img.htmlFor,
|
check.name,
|
check.id,
|
check.checked,
|
image.src,
|
image.alt,
|
] = [
|
item.id,
|
item.id,
|
item.id,
|
true,
|
item.images[item.fields['post_thumbnail']].medium,
|
item.images[item.fields['post_thumbnail']].alt,
|
];
|
selected.append(img);
|
}
|
}
|
|
renderBulkEditModal(modal) {
|
|
}
|
|
closeBulkEditModal(e, changes) {
|
for (let id of this.selected) {
|
//Remove any selected that are not selected from the bulk editor
|
if (!changes[id]) {
|
this.selected.delete(id);
|
}
|
//Remove the selected from the changes, because we don't need to send that to the server
|
delete changes[id];
|
}
|
if (window.isEmptyObject(changes) || this.selected.size === 0) {
|
return;
|
}
|
let temp = {};
|
for (let key in changes) {
|
|
let value = changes[key];
|
key = key.replace('bulk-edit-', '');
|
temp[key] = value;
|
}
|
|
let data = {};
|
for (let id of this.selected){
|
data[id] = temp;
|
}
|
this.saveData(data);
|
|
|
let modal = document.querySelector('dialog.bulk-edit-modal');
|
window.removeChildren(modal);
|
modal.append(this.bulkEditModal);
|
}
|
|
handleBulkEditModalSave(changes) {
|
|
}
|
|
openCreateModal(modal) {
|
if (this.isTable) {
|
this.modals['create'].handleClose();
|
}
|
}
|
|
renderCreateModal(modal) {
|
|
}
|
|
closeCreateModal() {
|
|
let modal = document.querySelector('dialog.create-modal');
|
window.removeChildren(modal);
|
modal.append(this.createModal);
|
}
|
|
handleCreateModalSave(changes) {
|
|
let id = this.modals.create.modal.querySelector('input[name="form-id"]').value;
|
let data = {};
|
if (!Object.hasOwn(data, id)) {
|
data[id] = {content: this.content};
|
}
|
|
for (let [field, value] of Object.entries(changes)) {
|
if (field === 'image_temp') {
|
continue;
|
}
|
data[id][field] = value;
|
}
|
|
this.saveData(data);
|
}
|
|
saveData(data) {
|
if (data.length === 0 || window.isEmptyObject(data)) {
|
return;
|
}
|
|
for (var [id, value] of Object.entries(data)) {
|
data[id].content = this.content;
|
}
|
let title = (data.length > 1) ? this.plural : this.single;
|
let operation = {
|
endpoint: 'content',
|
headers: {
|
'action_nonce': jvbSettings.dash,
|
},
|
title: `Adding ${data.length} ${title} to Queue`,
|
popup: `Queuing ${title}...`,
|
data: {
|
posts: data,
|
}
|
};
|
|
this.queue.addToQueue(operation);
|
}
|
|
handleBulkEdit(status) {
|
this.loading.showLoading('Processing bulk changes...');
|
try {
|
let posts = {};
|
this.selected.forEach(postID => {
|
|
posts[postID] = {
|
content: this.content,
|
status: status
|
};
|
if (['delete', 'trash', 'restore'].includes(status)) {
|
this.elements.grid.querySelector(`[data-id="${postID}"]`).remove();
|
}
|
});
|
this.saveData(posts);
|
this.clearSelection();
|
} catch (error) {
|
console.error('Bulk operation failed: ', error);
|
} finally {
|
this.loading.hideLoading();
|
}
|
}
|
|
handleDateRangeOpen() {
|
|
}
|
|
handleDateRangeClose() {
|
let start = this.elements.dateRange.querySelector('.date-start');
|
let end = this.elements.dateRange.querySelector('.date-end');
|
let select = this.elements.dateRange.querySelector('.month-select');
|
}
|
|
|
handleMonthSelect(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);
|
this.modals.dateRange.handleClose();
|
}
|
}
|
|
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.filters.date = type ? {
|
range: {
|
after: start.toISOString(),
|
before: end.toISOString()
|
},
|
custom: type === 'custom'
|
} : {
|
range: null,
|
custom: false
|
};
|
|
this.updateClearFiltersButton();
|
this.page = 1;
|
this.loadContent(true);
|
}
|
|
updateClearFiltersButton() {
|
const hasFilters =
|
Object.keys(this.filters.taxonomies).length > 0 ||
|
this.filters.date.range !== null;
|
|
this.elements.clearButton.hidden = !hasFilters;
|
}
|
|
handleClearFilters() {
|
this.resetFilters(true);
|
}
|
|
/*****************************************************
|
*
|
* VIEW CONTROLS
|
*
|
*****************************************************/
|
/**
|
* Initialize visual view (grid/list)
|
*/
|
initVisualView(viewName) {
|
// Visual views are initialized on first load
|
const view = this.views.get(viewName);
|
view.initialized = true;
|
}
|
|
/**
|
* Activate visual view
|
*/
|
activateVisualView(viewName) {
|
const view = this.views.get(viewName);
|
|
// Remove all view classes
|
this.elements.grid.classList.remove('grid-view', 'list-view', 'table');
|
|
// Add specific view class
|
this.elements.grid.classList.add(view.containerClass);
|
|
// Hide table-specific controls
|
if (this.elements.columnsSelect) {
|
this.elements.columnsSelect.hidden = true;
|
}
|
|
return Promise.resolve();
|
}
|
|
/**
|
* Deactivate visual view
|
*/
|
deactivateVisualView(viewName) {
|
// Clear rendered items if needed
|
return Promise.resolve();
|
}
|
|
/**
|
* Initialize table view
|
*/
|
initTableView() {
|
const view = this.views.get('table');
|
|
if (view.initialized) {
|
return;
|
}
|
|
// Create table container structure
|
const tableTemplate = window.getTemplate('contentTable');
|
if (!tableTemplate) {
|
throw new Error('Table template not found');
|
}
|
|
view.tableContainer = tableTemplate;
|
view.initialized = true;
|
}
|
|
/**
|
* Activate table view
|
*/
|
async activateTableView() {
|
const view = this.views.get('table');
|
|
// Initialize if needed
|
if (!view.initialized) {
|
this.initTableView();
|
}
|
|
// Remove visual view classes
|
this.elements.grid.classList.remove('grid-view', 'list-view');
|
this.elements.grid.classList.add('table');
|
|
// Insert table before grid
|
if (!this.elements.gridWrap.querySelector('form.table')) {
|
this.elements.gridWrap.insertBefore(
|
view.tableContainer.cloneNode(true),
|
this.elements.grid
|
);
|
}
|
|
// Get table reference
|
const table = this.elements.gridWrap.querySelector('form.table');
|
|
// Initialize the vertical navigation checkbox
|
const verticalCheckbox = table.querySelector('input#vertical');
|
if (verticalCheckbox) {
|
// Load saved preference
|
const savedPref = localStorage.getItem('jvbTabNav');
|
this.viewSettings.tabNav = savedPref === 'vertical';
|
verticalCheckbox.checked = this.viewSettings.tabNav;
|
}
|
|
// Register form with jvbForm
|
if (window.jvbForm && !view.tableForm) {
|
view.tableForm = window.jvbForm.registerForm(table, {
|
onSave: (data) => this.handleTableSave(data),
|
isRow: true,
|
content: this.config.content,
|
autoSave: true,
|
saveDelay: 3000
|
});
|
}
|
|
// Show column controls
|
if (this.elements.columnsSelect) {
|
this.elements.columnsSelect.hidden = false;
|
}
|
|
// Load saved column preferences
|
await this.loadTableColumns();
|
|
// Add keyboard navigation listeners
|
this.addTableListeners();
|
|
// Mark table view as active
|
this.isTable = true;
|
}
|
|
|
/**
|
* Deactivate table view
|
*/
|
async deactivateTableView() {
|
const view = this.views.get('table');
|
|
// Remove table listeners
|
this.removeTableListeners();
|
|
// Unregister form
|
if (view.tableForm && window.jvbForm) {
|
window.jvbForm.removeForm(view.tableForm.id);
|
view.tableForm = null;
|
}
|
|
// Remove table from DOM
|
const table = this.elements.gridWrap.querySelector('form.table');
|
if (table) {
|
table.remove();
|
}
|
|
// Hide column controls
|
if (this.elements.columnsSelect) {
|
this.elements.columnsSelect.hidden = true;
|
}
|
|
// Remove table class
|
this.elements.grid.classList.remove('table');
|
|
// Mark table view as inactive
|
this.isTable = false;
|
}
|
|
/**
|
* Cleanup table view (full cleanup)
|
*/
|
cleanupTableView() {
|
const view = this.views.get('table');
|
view.initialized = false;
|
view.tableContainer = null;
|
view.tableForm = null;
|
}
|
|
/**
|
* Render items for visual views
|
*/
|
renderVisualItems(items, viewName, append) {
|
const view = this.views.get(viewName);
|
const template = view.template;
|
|
if (!append){
|
// Clear existing items
|
window.removeChildren(this.elements.grid);
|
}
|
|
// Batch render items
|
const fragment = document.createDocumentFragment();
|
|
items.forEach(item => {
|
const element = this.createVisualElement(item, template);
|
fragment.appendChild(element);
|
});
|
|
this.elements.grid.appendChild(fragment);
|
|
// Make items keyboard navigable
|
if (window.jvbA11y) {
|
window.jvbA11y.makeNavigable(
|
this.elements.grid.querySelectorAll('.item:not([data-keyboard-nav])')
|
);
|
}
|
}
|
|
/**
|
* Render items for table view
|
*/
|
renderTableItems(items, append) {
|
const table = this.elements.gridWrap.querySelector('form.table tbody');
|
if (!table) return;
|
|
if (!append) {
|
// Clear existing rows
|
window.removeChildren(table);
|
}
|
|
// Batch render rows
|
const fragment = document.createDocumentFragment();
|
|
items.forEach(item => {
|
const row = this.createTableRow(item);
|
fragment.appendChild(row);
|
});
|
|
table.appendChild(fragment);
|
|
window.loadTemplates();
|
|
// Process form changes
|
const view = this.views.get('table');
|
if (view.tableForm && window.jvbForm) {
|
window.jvbForm.processChanges(view.tableForm, {processChanges: false});
|
|
// Scan for selectors and uploaders
|
window.jvbSelector?.scanExistingFields();
|
window.jvbUploadManager?.scanExistingFields();
|
}
|
}
|
|
/**
|
* Create visual element (grid/list item)
|
*/
|
createVisualElement(item, templateName) {
|
const template = window.getTemplate(templateName);
|
|
// Set basic attributes
|
template.dataset.id = item.id;
|
template.dataset.img = item.thumbnail || '';
|
|
if (item.fields) {
|
template.dataset.fields = JSON.stringify(item.fields);
|
}
|
|
if (item.images) {
|
template.dataset.images = JSON.stringify(item.images);
|
}
|
|
if (item.status) {
|
template.classList.add(item.status);
|
template.dataset.status = item.status;
|
}
|
|
// Populate fields
|
this.populateItemFields(template, item);
|
|
return template;
|
}
|
|
/**
|
* Create table row
|
*/
|
createTableRow(item) {
|
const row = window.getTemplate('tableView');
|
|
row.dataset.id = item.id;
|
|
// Update IDs for form handling
|
row.querySelectorAll('[id]').forEach(element => {
|
const label = element.nextElementSibling?.tagName === 'LABEL'
|
? element.nextElementSibling
|
: element.previousElementSibling?.tagName === 'LABEL'
|
? element.previousElementSibling
|
: null;
|
|
element.id = `${item.id}|${element.id}`;
|
element.name = `${item.id}|${element.name}`;
|
|
// let templates = element.querySelectorAll('template');
|
// if (templates) {
|
// templates.forEach(template => {
|
// template.className = template.className+item.id;
|
// })
|
// }
|
|
if (label) {
|
label.htmlFor = element.id;
|
}
|
});
|
|
// Set status
|
if (item.status) {
|
const statusRadio = row.querySelector(`[name="status"][value="${item.status}"]`);
|
if (statusRadio) {
|
statusRadio.checked = true;
|
}
|
}
|
|
// Populate field values
|
if (item.fields) {
|
row.querySelectorAll('.field').forEach(field => {
|
const fieldName = field.dataset.field;
|
if (fieldName && item.fields[fieldName] !== undefined) {
|
window.jvbForm?.populateFieldValue(
|
field,
|
`${item.id}|${fieldName}`,
|
item.fields[fieldName],
|
item.images
|
);
|
}
|
});
|
}
|
|
return row;
|
}
|
|
/**
|
* Populate item fields for visual views
|
*/
|
populateItemFields(element, item) {
|
// Populate image
|
const img = element.querySelector('img');
|
if (img && item.thumbnail) {
|
img.src = item.thumbnail;
|
img.alt = item.fields?.post_title || `${this.config.content} image`;
|
}
|
|
// Populate field values
|
if (item.fields) {
|
Object.entries(item.fields).forEach(([fieldName, value]) => {
|
const fieldElement = element.querySelector(`.${fieldName}`);
|
if (fieldElement) {
|
if (fieldElement.classList.contains('images')) {
|
window.handleGalleryField?.(fieldElement, value);
|
} else if (fieldElement.querySelector('li')) {
|
window.handleListField?.(fieldElement, value);
|
} else {
|
window.handleTextField?.(fieldElement, value);
|
}
|
}
|
});
|
}
|
|
// Set checkbox values
|
const checkbox = element.querySelector('.select-checkbox');
|
if (checkbox) {
|
checkbox.id = `select-${item.id}`;
|
checkbox.name = `select-${item.id}`;
|
checkbox.value = item.id;
|
|
const label = checkbox.nextElementSibling;
|
if (label) {
|
label.htmlFor = checkbox.id;
|
}
|
}
|
}
|
|
/**
|
* Handle column visibility changes
|
*/
|
handleColumnVisibility(event) {
|
const columnClass = event.target.id;
|
const table = this.elements.gridWrap.querySelector('form.table');
|
|
if (table) {
|
table.querySelectorAll(`.${columnClass}`).forEach(cell => {
|
cell.hidden = !event.target.checked;
|
});
|
}
|
|
this.saveViewSettings();
|
}
|
|
/**
|
* Add table-specific listeners
|
*/
|
addTableListeners() {
|
|
|
// Tab navigation listener
|
this.tabListener = (e) => {
|
// Only handle tab in table view
|
if (this.view !== 'table') return;
|
// Check if we're in a table input/select/textarea
|
const isInTable = e.target.closest('form.table tbody');
|
if (!isInTable) return;
|
|
if (e.key === 'Tab' && this.viewSettings.tabNav) {
|
this.handleTableTabNavigation(e);
|
} else if ((e.ctrlKey || e.metaKey) && e.key === 'ArrowUp') {
|
e.preventDefault();
|
this.toggleTableNavDirection();
|
}
|
};
|
|
document.addEventListener('keydown', this.tabListener);
|
}
|
|
/**
|
* Remove table-specific listeners
|
*/
|
removeTableListeners() {
|
if (this.tabListener) {
|
document.removeEventListener('keydown', this.tabListener);
|
this.tabListener = null;
|
}
|
}
|
|
/**
|
* Handle table tab navigation
|
*/
|
handleTableTabNavigation(event) {
|
const table = this.elements.gridWrap.querySelector('form.table');
|
if (!table) return;
|
|
const currentElement = document.activeElement;
|
const currentCell = currentElement.closest('td');
|
if (!currentCell) return;
|
|
const currentRow = currentCell.closest('tr');
|
const rows = Array.from(table.querySelectorAll('tbody tr'));
|
const currentRowIndex = rows.indexOf(currentRow);
|
|
if (currentRowIndex === -1) return;
|
|
// Determine next row (up or down based on shift key)
|
const nextRowIndex = event.shiftKey ? currentRowIndex - 1 : currentRowIndex + 1;
|
|
// Check bounds
|
if (nextRowIndex < 0 || nextRowIndex >= rows.length) {
|
// Optionally, you could wrap around or stop
|
return;
|
}
|
|
const nextRow = rows[nextRowIndex];
|
if (!nextRow) return;
|
|
// Find the corresponding cell in the next row
|
const cells = Array.from(currentRow.querySelectorAll('td'));
|
const currentCellIndex = cells.indexOf(currentCell);
|
|
const nextRowCells = Array.from(nextRow.querySelectorAll('td'));
|
const nextCell = nextRowCells[currentCellIndex - 1];
|
|
if (!nextCell) return;
|
|
// Find the first focusable element in the next cell
|
const focusable = nextCell.querySelector('input, select, textarea, button');
|
|
if (focusable) {
|
// Smooth scroll to the next row
|
nextRow.scrollIntoView({
|
behavior: 'smooth',
|
block: 'nearest',
|
inline: 'nearest'
|
});
|
|
// Focus the element
|
focusable.focus();
|
|
// If it's a text input, select all text for easy editing
|
if (focusable.type === 'text' || focusable.type === 'number') {
|
focusable.select();
|
}
|
}
|
}
|
|
/**
|
* Toggle table navigation direction
|
*/
|
toggleTableNavDirection() {
|
this.viewSettings.tabNav = !this.viewSettings.tabNav;
|
|
const message = this.viewSettings.tabNav
|
? 'Changed to vertical navigation'
|
: 'Changed to horizontal navigation';
|
|
window.jvbA11y?.announce(message);
|
this.saveViewSettings();
|
}
|
|
/**
|
* Save view settings to cache
|
*/
|
async saveViewSettings() {
|
const settings = {
|
view: this.view,
|
tabNav: this.viewSettings.tabNav || false,
|
columns: []
|
};
|
|
// Save column visibility for table view
|
if (this.view === 'table') {
|
const checkedColumns = document.querySelectorAll('.multi-select input:checked');
|
settings.columns = Array.from(checkedColumns).map(input => input.id);
|
}
|
|
// Save to cache
|
if (window.jvbCache) {
|
await window.jvbCache.setItem(`${this.config.content}_view_settings`, settings);
|
}
|
|
// Also save to localStorage for quick access
|
localStorage.setItem(`${this.config.content}_view`, this.view);
|
}
|
|
/**
|
* Load view settings from cache
|
*/
|
async loadViewSettings() {
|
// Try cache first
|
if (window.jvbCache) {
|
const cached = await window.jvbCache.getItem(`${this.config.content}_view_settings`);
|
if (cached) {
|
this.viewSettings = cached;
|
return cached.view || 'grid';
|
}
|
}
|
|
// Fallback to localStorage
|
const savedView = localStorage.getItem(`${this.config.content}_view`);
|
return savedView || 'grid';
|
}
|
|
/**
|
* Load table column preferences
|
*/
|
async loadTableColumns() {
|
if (!this.viewSettings.columns || !this.viewSettings.columns.length) {
|
return;
|
}
|
|
const table = this.elements.gridWrap.querySelector('form.table');
|
if (!table) return;
|
|
// Apply saved column visibility
|
document.querySelectorAll('.multi-select input[type="checkbox"]').forEach(input => {
|
const isVisible = this.viewSettings.columns.includes(input.id);
|
input.checked = isVisible;
|
|
// Update column visibility
|
table.querySelectorAll(`.${input.id}`).forEach(cell => {
|
cell.hidden = !isVisible;
|
});
|
});
|
}
|
|
/**
|
* Handle table save
|
*/
|
handleTableSave(changes) {
|
this.saveData(changes);
|
}
|
|
/**
|
* Callback for view changes
|
*/
|
onViewChange(viewName) {
|
if (this.config.onViewChange) {
|
this.config.onViewChange(viewName);
|
}
|
}
|
|
/**
|
* Get current view
|
*/
|
getCurrentView() {
|
return this.view;
|
}
|
|
/**
|
* Get view object
|
*/
|
getView(viewName) {
|
return this.views.get(viewName);
|
}
|
|
/**
|
* Check if current view is editable
|
*/
|
isEditableView() {
|
const view = this.views.get(this.view);
|
return view && view.type === 'editable';
|
}
|
|
/**
|
* Render items based on current view
|
*/
|
renderItems(items, append = false) {
|
const view = this.views.get(this.view);
|
if (view && view.render) {
|
view.render(items, append);
|
}
|
}
|
|
/**
|
* Add empty row (for table view)
|
*/
|
addEmptyRow() {
|
if (this.view !== 'table') {
|
return;
|
}
|
|
const table = this.elements.gridWrap.querySelector('form.table tbody');
|
if (!table) return;
|
|
// Remove empty state if present
|
this.elements.container.querySelector('.empty-state')?.remove();
|
|
// Create new row
|
const template = window.getTemplate('tableView');
|
const timestamp = new Date().getTime().toString(36);
|
|
template.dataset.id = `new-${timestamp}`;
|
|
// Update IDs for new row
|
template.querySelectorAll('[id]').forEach(element => {
|
const label = element.nextElementSibling?.tagName === 'LABEL'
|
? element.nextElementSibling
|
: element.previousElementSibling?.tagName === 'LABEL'
|
? element.previousElementSibling
|
: null;
|
|
element.id = `${timestamp}|${element.id}`;
|
element.name = `${timestamp}|${element.name}`;
|
|
if (label) {
|
label.htmlFor = element.id;
|
}
|
});
|
|
// Handle selectors
|
template.querySelectorAll('.jvb-selector')?.forEach(selector => {
|
selector.id = `${timestamp}-${selector.id}`;
|
const toggle = selector.querySelector('.taxonomy-toggle');
|
if (toggle && window.jvbSelector) {
|
window.jvbSelector.handleToggleClick(toggle, false);
|
}
|
});
|
|
table.appendChild(template);
|
|
// Focus first input
|
const firstInput = template.querySelector('input:not([type="checkbox"]), select, textarea');
|
if (firstInput) {
|
firstInput.focus();
|
}
|
}
|
|
cleanup() {
|
// Remove listeners
|
this.removeTableListeners();
|
|
// Cleanup views
|
this.views.forEach((view, name) => {
|
if (view.cleanup) {
|
view.cleanup();
|
}
|
});
|
|
// Clear references
|
this.views.clear();
|
this.elements = null;
|
this.config = null;
|
|
|
this.posts.clear();
|
this.selected.clear();
|
|
document.removeEventListener('click', this.clickHandler);
|
document.removeEventListener('change', this.changeHandler);
|
document.removeEventListener('keydown', this.keyHandler);
|
document.removeEventListener('keydown', this.handleEscape);
|
document.removeEventListener('keydown', this.tabListener);
|
}
|
}
|
|
window.crud = CRUD;
|
window.addEventListener('beforeunload', () => window.crud?.cleanup());
|