class CRUDManager {
|
constructor(){
|
this.container = document.querySelector('.crud[data-content]:not([data-ignore])');
|
if (!this.container) return;
|
this.content = this.container.dataset.content;
|
this.endpoint = this.container.dataset.endpoint??'content';
|
this.singular = this.container.dataset.singular;
|
this.plural = this.container.dataset.plural;
|
|
this.queue = window.jvbQueue;
|
this.a11y = window.jvbA11y;
|
this.error = window.jvbError;
|
this.populate = window.jvbPopulate;
|
this.cache = new window.jvbCache(this.content);
|
|
this.activeItem = null;
|
this.isTimeline = false;
|
this.isPopulating = false;
|
|
//Track Changes
|
this.changes = new Map();
|
this.items = new Map();
|
|
this.init();
|
}
|
|
init() {
|
this.initElements();
|
this.initListeners();
|
this.defineTemplates();
|
let cached = this.initSettings();
|
this.initStore(cached);
|
this.checkHideFilters();
|
this.initIntegrations();
|
this.initUploader();
|
this.initModals();
|
}
|
|
defineTemplates() {
|
const T = window.jvbTemplates;
|
const crud = this;
|
|
const baseSetup = (el, refs, data) => {
|
el.dataset.itemId = data.id;
|
let wrapper = refs.checkbox.closest('.preview');
|
window.prefixInput(refs.checkbox, `select-${data.id}`, wrapper, true);
|
refs.checkbox.value = data.id;
|
refs.checkbox.checked = crud.selected.has(parseInt(data.id));
|
if (refs.selectLabel) refs.selectLabel.htmlFor = `select-${data.id}`;
|
|
if (refs.edit) refs.edit.dataset.id = data.id;
|
if (refs.trash) refs.trash.dataset.id = data.id;
|
};
|
const imageSetup = function(el, refs, data) {
|
let hasThumbnail = data?.fields?.post_thumbnail || data?.fields?.thumbnail;
|
if (hasThumbnail) {
|
const thumbnail = data.images[hasThumbnail] ?? {};
|
refs.img.src = thumbnail.medium??'';
|
refs.img.alt = thumbnail.alt??data.fields.post_title??'';
|
}
|
|
}
|
|
T.define('gridView', {
|
refs: {
|
img: 'img',
|
checkbox: '.select-item',
|
selectLabel: 'label.select-item-label',
|
edit: '[data-action="edit"]',
|
trash: '[data-action="trash"]'
|
},
|
setup({ el, refs, manyRefs, data }) {
|
baseSetup(el, refs, data);
|
imageSetup(el, refs, data);
|
}
|
});
|
|
T.define('listView', {
|
refs: {
|
img: 'img',
|
checkbox: '.select-item',
|
selectLabel: 'label.select-item-label',
|
edit: '[data-action="edit"]',
|
trash: '[data-action="trash"]'
|
},
|
manyRefs: {
|
attrs: '[data-attr]',
|
fields: '[data-field]'
|
},
|
setup({ el, refs, manyRefs, data }) {
|
baseSetup(el, refs, data);
|
imageSetup(el, refs, data);
|
manyRefs?.attrs?.forEach(el => {
|
const value = data[el.dataset.attr];
|
if (value && value !=='') {
|
el.textContent = value;
|
} else {
|
el.remove();
|
}
|
});
|
manyRefs?.fields?.forEach(el => {
|
const value = data.fields?.[el.dataset.field];
|
if (value && value !== '') {
|
el.tagName === 'DIV' ? el.innerHTML = value : el.textContent = value;
|
} else {
|
el.remove();
|
}
|
});
|
}
|
});
|
|
let tableRefs = {};
|
let tableMany = {};
|
if (this.isTimeline) {
|
tableRefs.sharedRow = 'tr.shared';
|
tableRefs.point = 'tr.timeline-point';
|
}
|
T.define('tableView', {
|
refs: {
|
checkbox: '.select-item',
|
selectLabel: 'label.select-item-label',
|
... tableRefs,
|
},
|
manyRefs: {
|
inputs: 'input,select,textarea',
|
status: 'input[name="post_status"]',
|
selectors: '[data-type="selector"]',
|
fields: '[data-field]',
|
... tableMany,
|
},
|
setup({ el, refs, manyRefs, data }) {
|
baseSetup(el, refs, data);
|
|
manyRefs?.inputs?.forEach(el => {
|
let wrapper = el.closest('[data-field]');
|
window.prefixInput(el, `${data.id}-`, wrapper);
|
});
|
|
manyRefs?.status?.forEach(el => {
|
if (el.value === data.status) {
|
el.checked = true;
|
}
|
});
|
|
if (crud.isTimeline) {
|
if (refs.sharedRow) {
|
refs.sharedRow.querySelectorAll('input,select,textarea').forEach(input => {
|
let wrapper = input.closest('[data-field]');
|
window.prefixInput(input, `${data.id}-`, wrapper);
|
});
|
|
crud.populate.populate(refs.sharedRow, data);
|
|
// Handle status radios in shared row
|
refs.sharedRow.querySelectorAll('input[name="post_status"]').forEach(el => {
|
if (el.value === data.status) {
|
el.checked = true;
|
}
|
});
|
}
|
|
if (refs.point && data.fields?.timeline) {
|
|
Object.entries(data.fields.timeline).forEach(([nuthing, timeline], index) => {
|
const point = refs.point.cloneNode(true);
|
point.dataset.index = `${index}`;
|
point.dataset.itemId = timeline.id;
|
|
point.querySelectorAll('input,select,textarea').forEach(input => {
|
let wrapper = input.closest('[data-field]');
|
window.prefixInput(input, `${timeline.id}-`, wrapper);
|
});
|
|
crud.populate.populate(point, {
|
fields: timeline,
|
images: data.images,
|
taxonomies: data.taxonomies
|
});
|
|
const imgData = data.images?.[timeline.post_thumbnail];
|
if (imgData) {
|
point.querySelector('.field.upload')?.setAttribute('title', imgData['image-title']??'');
|
}
|
el.insertBefore(point, refs.point);
|
});
|
refs.point.remove();
|
}
|
} else {
|
if (crud.ui.table.form?.dataset.edit !== undefined) {
|
// Non-timeline: prefix all inputs normally
|
manyRefs?.inputs?.forEach(input => {
|
let wrapper = input.closest('[data-field]');
|
window.prefixInput(input, `${data.id}-`, wrapper);
|
});
|
|
manyRefs?.status?.forEach(el => {
|
if (el.value === data.status) {
|
el.checked = true;
|
}
|
});
|
|
crud.populate.populate(el, data);
|
} else {
|
const fields = (Object.hasOwn(data, 'fields')) ? data.fields : data;
|
manyRefs?.fields?.forEach(field => {
|
if (Object.hasOwn(fields, field.dataset.field) && fields[field.dataset.field] !== '') {
|
let value = fields[field.dataset.field];
|
let p = fields.children[0];
|
if (p) {
|
p.textContent = field.dataset.field === 'date'
|
? window.formatTimeAgo(value)
|
: value;
|
}
|
}
|
});
|
}
|
}
|
|
manyRefs?.selectors?.forEach(selector => selector.setAttribute('data-lazy', ''));
|
}
|
});
|
|
T.define('emptyState');
|
|
T.define('bulkItem', {
|
refs: {
|
checkbox: 'input',
|
img: 'img',
|
label: 'label'
|
},
|
setup({el, refs, manyRefs, data}) {
|
if (refs.checkbox) {
|
refs.checkbox.id = `bulk_${data.id}`;
|
refs.checkbox.value = data.id;
|
refs.checkbox.checked = true;
|
refs.checkbox.name ='selected[]';
|
}
|
let thumbnail = data?.images[data?.fields?.post_thumnbail]??{};
|
if (refs.img && Object.keys(thumbnail).length >0) {
|
refs.img.src = thumbnail.medium??'';
|
refs.img.alt = thumbnail.alt??'';
|
}
|
|
if (refs.label) {
|
refs.label.title = item.fields.post_title;
|
}
|
}
|
});
|
T.define('trashOptions');
|
T.define('notTrashOptions');
|
T.define('contentTable');
|
}
|
|
initElements() {
|
this.allowedFilters = ['status', 'orderby', 'order', 'search', 'date-filter', 'dateFrom', 'dateTo'];
|
this.selectors = {
|
buttons: {
|
create: '.create-item',
|
clearFilters: '[data-action="clear-filters"]'
|
},
|
views: {
|
grid: 'input[data-view="grid"]',
|
list: 'input[data-view="list"]',
|
table: 'input[data-view="table"]'
|
},
|
modals: {
|
create: {
|
modal: 'dialog.create',
|
form: 'dialog.create form',
|
h2: 'dialog.create h2',
|
},
|
edit: {
|
modal: 'dialog.edit',
|
form: 'dialog.edit form',
|
h2: 'dialog.edit h2',
|
},
|
bulkEdit: {
|
modal: 'dialog.bulkEdit',
|
selected: 'dialog.bulkEdit .selected',
|
h2: 'dialog.bulkEdit h2 span',
|
form: 'dialog.bulkEdit form'
|
},
|
date: {
|
modal: 'dialog.date-range',
|
start: 'dialog.date-range .date-start',
|
end: 'dialog.date-range .date-end',
|
month: 'dialog.date-range .month-select',
|
}
|
},
|
grid: `.${this.content}.item-grid`,
|
table: {
|
nav: '#vertical',
|
form: 'form.table',
|
table: 'form.table table',
|
body: 'form.table body',
|
head: 'form.table thead',
|
foot: 'form.table tfoot',
|
selectedColumns: '.all-filters .multi-select',
|
columns: 'thead th',
|
},
|
bulk: {
|
action: '.bulk-action-select',
|
count: '.bulk-controls .selected-count',
|
control: '.bulk-controls .bulk-actions',
|
select: '.bulk-controls select',
|
selectAll: '.select-all'
|
},
|
filters: {
|
container: 'details.all-filters',
|
search: '.all-filters input[type="search"]',
|
status: {
|
all: '[name="status"]#all',
|
publish: '[name="status"]#publish',
|
draft: '[name="status"]#draft',
|
trash: '[name="status"]#trash',
|
},
|
orderby: {
|
date: '[name="orderby"]#date',
|
alphabetical: '[name="orderby"]#alphabetical',
|
},
|
order: {
|
asc: '[name="order"][value="asc"]',
|
desc: '[name="order"][value="desc"]'
|
},
|
date: '[data-filter="date"]'
|
},
|
uploader: 'details.uploader'
|
}
|
|
this.ui = window.uiFromSelectors(this.selectors);
|
const taxFilters = document.querySelectorAll('[data-filter="taxonomies"]');
|
if (taxFilters.length > 0) {
|
this.ui.filters.taxonomies = {};
|
taxFilters.forEach(tax => {
|
const taxonomy = tax.dataset.taxonomy;
|
this.ui.filters.taxonomies[taxonomy] = tax;
|
this.allowedFilters.push(`tax_${taxonomy}`);
|
});
|
}
|
this.isTimeline = !!document.querySelector('[data-timeline]');
|
}
|
initUploader() {
|
if (!this.ui.uploader) return;
|
|
window.jvbUploads.scanFields(this.ui.uploader);
|
window.jvbUploads.subscribe((event, data) => {
|
if (event === 'sent-to-queue') {
|
if (data === this.ui.uploader.dataset.uploader) {
|
window.debouncer.schedule('crud-complete', ()=> {
|
this.store.clearCache();
|
});
|
}
|
}
|
|
if (event === 'sent-to-queue' && data.field) {
|
const fieldName = data.field.config.name;
|
const itemId = data.field.config.itemID;
|
if (itemId && fieldName) {
|
if (this.changes.has(itemId)) {
|
delete this.changes.get(itemId)[fieldName];
|
}
|
}
|
}
|
});
|
}
|
initModals() {
|
this.modals = {};
|
for (let [name, modal] of Object.entries(this.ui.modals)) {
|
if (!modal.modal) continue;
|
this.modals[name] = new window.jvbModal(modal.modal);
|
|
this.modals[name].subscribe((event, data) => {
|
switch (event) {
|
case 'modal-close':
|
const formId = this.ui.modals[name].form.dataset.formId;
|
if (formId) {
|
this.forms.clearForm(formId);
|
}
|
|
this.resetForm(this.ui.modals[name].form);
|
|
if (name === 'date') {
|
this.handleCustomDateSelection()
|
}
|
if (['edit','bulkEdit','create'].includes(name)) {
|
//handle escapes (not form submits)
|
if (window.debouncer.timeouts.has(`save-${this.content}`)) {
|
this.scheduleSave(0);
|
}
|
}
|
break;
|
case 'modal-open':
|
|
break;
|
}
|
})
|
}
|
|
}
|
|
initStore(cached) {
|
let filters = {
|
... this.defaults,
|
...cached
|
};
|
|
|
const stores = window.jvbStore.register(
|
this.content,
|
[
|
{
|
storeName: this.content,
|
keyPath: 'id',
|
endpoint: this.endpoint??'content', //for taxonomy stores
|
headers: {
|
'X-Action-Nonce': window.auth.getNonce('dash'),
|
},
|
indexes: [
|
{name: 'id', keyPath: 'id'},
|
{ name: 'status', keyPath: 'status'},
|
{ name: 'date', keyPath: 'date'},
|
{ name: 'modified', keyPath: 'modified'},
|
{ name: 'title', keyPath: 'title'},
|
],
|
isAuth: true,
|
filters: filters,
|
ignore: ['content', 'user'],
|
TTL: 60 * 60 * 1000, //1 hour cache
|
showLoading: true,
|
},
|
{
|
storeName: 'changes',
|
keyPath: 'id'
|
}
|
]
|
);
|
|
this.changesStore = stores['changes'];
|
this.store = stores[this.content];
|
|
this.store.subscribe((event, data) => {
|
switch (event) {
|
case 'data-loaded':
|
this.render();
|
this.selectionHandler.collectItems();
|
break;
|
}
|
});
|
|
this.changesStore.subscribe((event, data) => {
|
switch (event) {
|
case 'data-ready':
|
let changes = this.changesStore.getAll();
|
if (changes.length > 0) {
|
changes.forEach(change => {
|
this.changes.set(change.id, change);
|
});
|
this.savePosts('', false).then(()=>{});
|
}
|
break;
|
}
|
});
|
}
|
initIntegrations() {
|
this.selected = new Set();
|
this.selectionHandler = new window.jvbHandleSelection(this.container, {
|
selectAll: {
|
checkbox: '#select-all',
|
label: '.bulk-select label',
|
span: '.bulk-select label span'
|
},
|
wrapper: {
|
wrapper: '.wrap'
|
},
|
item: {
|
idAttribute: 'itemId'
|
}
|
});
|
this.selectionHandler.subscribe((event, data) => {
|
this.selected = new Set([...data.selectedItems].map(id => parseInt(id)));
|
this.ui.bulk.control.hidden = this.selected.size === 0;
|
this.ui.bulk.count.hidden = this.selected.size === 0;
|
this.ui.bulk.count.textContent = `${this.selected.size} ${this.plural} selected`;
|
});
|
|
this.forms = window.jvbForm;
|
|
// this.forms.subscribe((event, data) => {
|
// switch(event) {
|
// case 'form-submit':
|
// case 'form-autosave':
|
// // this.handleFormChange(event,data);
|
// break;
|
// }
|
// });
|
|
if (window.jvbUploads) {
|
window.jvbUploads.subscribe((event, data) => {
|
if (event === 'groups_uploaded' && data.content === this.content) {
|
this.handleGroupsUploaded(data);
|
}
|
});
|
}
|
|
this.queue.subscribe((event, data) => {
|
if (['image_upload', 'video_upload', 'document_upload'].includes(data.type)
|
&& event === 'operation-status'
|
&& data.status === 'completed') {
|
this.store.clearCache();
|
}
|
|
|
if (event === 'operation-status'
|
&& data.status === 'completed'
|
&& data.endpoint === 'uploads/groups') {
|
if (data.result && data.result.group_mappings) {
|
console.log('Handling group mapping from queue response');
|
this.handleGroupMappings(data.result.group_mappings);
|
}
|
this.store.clearCache();
|
}
|
|
if (event === 'operation-status'
|
&& data.status === 'completed'
|
&& data.type === 'content_update') {
|
|
this.store.clearCache();
|
|
if (!data.result || !data.result.success || !data.result.errors)
|
{
|
console.warn('Content update completed but no results', data);
|
return;
|
}
|
|
if (Object.keys(data.result.success).length > 0) {
|
this.checkCompletedChanges(Object.entries(data.result.success));
|
}
|
if (Object.keys(data.result.errors).length > 0) {
|
this.checkFailedChanges(Object.entries(data.result.errors));
|
return;
|
}
|
|
if (Object.keys(data.result.success).length === 0) {
|
console.log(data.result.success);
|
data.result.success.forEach(id => this.changesStore.delete(id));
|
|
this.store.clearCache();
|
}
|
}
|
|
if (event === 'sent-to-server') {
|
console.log('Sent to server in CRUD.js with data: ', data);
|
for ( let [id, changes] of Object.entries(data.posts)) {
|
this.compareStored(id, changes);
|
}
|
}
|
|
});
|
}
|
checkCompletedChanges(items) {
|
for (let [id, data] of items) {
|
this.compareStored(id, data);
|
}
|
}
|
compareStored(id, data) {
|
let stored = this.changesStore.get(id);
|
if (!stored) return;
|
|
for (let [field, value] of Object.entries(data)) {
|
if (Object.hasOwn(stored, field)) {
|
let changes = window.getDifferences.map(stored[field], value);
|
if (!changes) {
|
delete stored[field];
|
} else {
|
stored[field] = changes;
|
}
|
}
|
}
|
|
let hasID = Object.hasOwn(stored, 'id');
|
let hasContent = Object.hasOwn(stored, 'content');
|
if ((hasID && hasContent && Object.keys(stored).length === 2)
|
|| ((hasID || hasContent) && Object.keys(stored).length === 1)
|
|| Object.keys(stored).length === 0
|
) {
|
this.changesStore.delete(id);
|
this.store.clearCache();
|
} else {
|
this.changesStore.save(stored);
|
}
|
}
|
checkFailedChanges(items) {
|
//TODO do something.
|
}
|
|
initSettings() {
|
this.defaults = {
|
content: this.content,
|
user: window.auth.getUser(),
|
page: 1,
|
status: 'all',
|
orderby: 'date',
|
order: 'desc',
|
search: '',
|
}
|
|
let updateFilters = {};
|
//current view (defaults to grid)
|
let defaultView = this.container.dataset.view??'grid'
|
this.view = this.cache.get('view')??defaultView;
|
if (this.view !== defaultView) {
|
this.ui.views[this.view].checked = true;
|
}
|
//current status (defaults to all)
|
this.status = this.cache.get('status')??this.defaults.status;
|
if (this.status !== this.defaults.status) {
|
this.ui.filters.status[this.status].checked = true;
|
updateFilters.status = this.status;
|
}
|
//orderby & order
|
this.orderby = this.cache.get('orderby')??this.defaults.orderby;
|
if (this.orderby !== this.defaults.orderby) {
|
this.ui.filters.orderby[this.orderby].checked = true;
|
updateFilters.orderBy = this.orderby;
|
}
|
this.order = this.cache.get('order')??this.defaults.order;
|
if (this.order !== this.defaults.order) {
|
this.ui.filters.order[this.order].checked = true;
|
updateFilters.order = this.order;
|
}
|
|
if (this.ui.filters.taxonomies) {
|
Object.entries(this.ui.filters.taxonomies).forEach(([taxonomy, element]) => {
|
const filterKey = `tax_${taxonomy}`;
|
const cached = this.cache.get(filterKey);
|
if (cached) {
|
element.value = cached;
|
updateFilters[filterKey] = cached;
|
}
|
});
|
}
|
|
let tabDirection = this.cache.get('tabNav')??'horizontal';
|
if (this.ui.table.nav && tabDirection === 'vertical') {
|
this.ui.table.nav.checked = true;
|
}
|
|
|
|
//Setup details open functionality
|
let details = {
|
showFilters: {
|
element: this.ui.filters.container,
|
default: 'closed',
|
},
|
showUploader: {
|
element: this.ui.uploader,
|
default: 'open'
|
}
|
};
|
for (let [name, conf] of Object.entries(details)) {
|
if (conf.element) {
|
let cached = this.cache.get(name)??conf.default;
|
conf.element.open = cached === 'open';
|
conf.element.addEventListener('toggle', ()=> {
|
this.cache.set(name, conf.element.open ? 'open' : 'closed');
|
});
|
}
|
}
|
|
return updateFilters;
|
}
|
/****************************************************************
|
EVENT LISTENERS
|
****************************************************************/
|
initListeners() {
|
this.changeHandler = this.handleChange.bind(this);
|
this.clickHandler = this.handleClick.bind(this);
|
this.inputHandler = this.handleInput.bind(this);
|
this.submitHandler = this.handleModalSubmit.bind(this);
|
|
document.addEventListener('change', this.changeHandler);
|
document.addEventListener('click', this.clickHandler);
|
if (this.ui.filters.search) {
|
this.ui.filters.search.addEventListener('input', this.inputHandler);
|
}
|
|
for (let [name, modal] of Object.entries(this.ui.modals)) {
|
if (modal.form) {
|
modal.form.addEventListener('submit', this.submitHandler);
|
}
|
}
|
}
|
|
handleModalSubmit(e) {
|
e.preventDefault();
|
const form = e.target;
|
const modal = form.closest('dialog');
|
if (!modal) return;
|
|
if (modal.classList.contains('create')) {
|
this.handleCreateSubmit(modal);
|
return;
|
}
|
|
let title = `Saving changes for multiple ${this.plural}`;
|
|
this.scheduleSave(0);
|
this.modals.edit.handleClose();
|
}
|
|
async handleCreateSubmit(modal) {
|
const itemId = modal.dataset.itemId;
|
|
// 1. Flush changes to store
|
if (this.changes.size > 0) {
|
this.cancelBackup();
|
await this.handleBackup();
|
}
|
|
const changes = await this.changesStore.getAll();
|
if (changes.length === 0) return;
|
|
let allChanges = {};
|
changes.forEach(change => {
|
const { id, ...rest } = change;
|
allChanges[id] = rest;
|
});
|
|
// 2. Queue content creation, get operationId
|
let contentOpId = this.queue.addToQueue({
|
endpoint: this.endpoint,
|
headers: {
|
'X-Action-Nonce': window.auth.getNonce('dash'),
|
},
|
data: {
|
posts: allChanges,
|
},
|
popup: `Creating your new ${this.singular}`,
|
title: `Creating your new ${this.singular}`,
|
});
|
|
if (!contentOpId) return;
|
|
// 3. Queue any pending uploads with dependency on content creation
|
const uploadFields = modal.querySelectorAll('[data-upload-field]');
|
for (const fieldEl of uploadFields) {
|
const fieldId = fieldEl.dataset.uploader;
|
if (!fieldId) continue;
|
|
const uploads = window.jvbUploads.stores.uploads.filterByIndex({ field: fieldId });
|
if (uploads.length === 0) continue;
|
|
await window.jvbUploads.queueUploads('uploads', fieldId, contentOpId);
|
}
|
}
|
handleChange(e) {
|
// Early bailout - target must be in an item or be a filter
|
const inItem = e.target.closest('[data-item-id]');
|
const isFilter = e.target.matches('[data-filter]');
|
const isBulkAction = e.target.matches('.bulk-action-select');
|
const isView = e.target.matches('[data-view]');
|
|
if (!inItem && !isFilter && !isBulkAction && !isView) return;
|
|
if (!this.isPopulating && inItem && !e.target.closest('[data-ignore], .select-item')) {
|
this.handleItemUpdate(e);
|
return;
|
}
|
|
if (isView) {
|
this.items.clear();
|
this.handleViewChange(e.target);
|
return;
|
}
|
|
if (isBulkAction) {
|
this.handleBulkAction(e.target);
|
return;
|
}
|
|
if (isFilter) {
|
this.handleFilterChange(e.target);
|
return;
|
}
|
|
// Table-specific handlers
|
if (this.view === 'table') {
|
if (e.target.matches('details.multi-select')) {
|
this.toggleColumn(e.target.id, e.target.checked);
|
return;
|
}
|
|
if (e.target.matches(this.selectors.table.nav)) {
|
this.tabNav = e.target.checked;
|
this.cache.set('tabNav', e.target.checked ? 'vertical' : 'horizontal');
|
}
|
}
|
}
|
handleBulkAction(bulkAction) {
|
if (bulkAction.value.startsWith('tax-')) {
|
const selectedOption = bulkAction.options[bulkAction.selectedIndex];
|
const taxonomy = selectedOption.dataset.taxonomy;
|
const single = selectedOption.dataset.single;
|
const plural = selectedOption.dataset.plural;
|
|
window.jvbSelector.openEmpty(
|
taxonomy,
|
single,
|
plural,
|
(result) => this.handleBulkTaxonomy(result)
|
);
|
bulkAction.value = '';
|
|
return;
|
}
|
switch(bulkAction.value) {
|
case 'edit':
|
this.openBulkEditModal();
|
break;
|
case 'publish':
|
case 'trash':
|
case 'delete':
|
this.setBulkStatus(bulkAction.value);
|
break;
|
case 'draft':
|
case 'restore':
|
this.setBulkStatus('draft');
|
break;
|
}
|
}
|
handleBulkTaxonomy(result) {
|
if (!result.termIds.length || !this.selected.size) return;
|
|
this.selected.forEach(itemID => {
|
const item = this.store.get(itemID);
|
if (!item) return;
|
|
// Merge existing terms with new ones
|
const existingTerms = item.taxonomies?.[result.taxonomy] || [];
|
const existingIds = existingTerms.map(t => t.id);
|
const newIds = [...new Set([...existingIds, ...result.termIds])];
|
|
this.updateItem(itemID, result.taxonomy, newIds);
|
});
|
|
this.savePosts(`Adding ${result.terms.length} ${result.taxonomy} to ${this.selected.size} ${this.plural}...`,).then(()=> {});
|
|
this.selectionHandler.clearSelection();
|
}
|
|
handleItemUpdate(e) {
|
let item = window.targetCheck(e, '[data-item-id]');
|
if (!item) return;
|
|
// Check if inside a collection field first
|
const collection = e.target.closest('[data-field-type="repeater"], [data-field-type="tag-list"]');
|
|
let name, value;
|
if (collection) {
|
name = collection.dataset.field;
|
value = this.forms.getFieldValue(collection);
|
} else {
|
let field = e.target.closest('[data-field]');
|
name = field.dataset.field;
|
value = this.forms.getFieldValue(e.target);
|
}
|
|
console.log('Name: ', name);
|
console.log('Value: ', value);
|
|
item.dataset.itemId.split(',').forEach(itemId => {
|
this.updateItem(itemId, name, value);
|
});
|
}
|
updateItem(itemId, name, value) {
|
if (this.isPopulating) {
|
return;
|
}
|
name.replace(`[${itemId}]`, '');
|
|
const stored = this.store.get(itemId);
|
if (stored) {
|
const storedValue = stored.fields?.[name] ?? stored[name];
|
const diff = window.getDifferences.map(storedValue, value);
|
|
if (diff === null) {
|
// Value matches stored — clean up any pending change for this field
|
if (this.changes.has(itemId)) {
|
delete this.changes.get(itemId)[name];
|
// If no real changes left, remove the item entirely
|
const remaining = Object.keys(this.changes.get(itemId))
|
.filter(k => k !== 'id' && k !== 'content');
|
if (remaining.length === 0) {
|
this.changes.delete(itemId);
|
this.changesStore.delete(itemId);
|
}
|
}
|
return;
|
}
|
}
|
|
if (!this.changes.has(itemId)) {
|
this.changes.set(itemId, { id: itemId, content: this.content });
|
}
|
this.changes.get(itemId)[name] = value;
|
|
this.scheduleBackup();
|
if (typeof itemId === 'number' || !String(itemId).includes('group')) {
|
this.scheduleSave();
|
}
|
}
|
scheduleBackup() {
|
window.debouncer.schedule(
|
`changes-${this.content}`,
|
async () => {
|
if (this.changes.size > 0) {
|
await this.handleBackup();
|
}
|
},
|
2000
|
);
|
}
|
cancelBackup() {
|
window.debouncer.cancel(`changes-${this.content}`);
|
}
|
async handleBackup() {
|
const changesArray = Array.from(this.changes.values());
|
this.changes.clear();
|
|
const ids = changesArray.map(c => c.id);
|
const existing = await Promise.all(
|
ids.map(id => this.changesStore.get(id))
|
);
|
|
const changes = changesArray.map((change, i) =>
|
existing[i] ? window.deepMerge(existing[i], change) : change
|
);
|
|
await this.changesStore.saveMany(changes);
|
}
|
|
scheduleSave(delay = 10000) {
|
window.debouncer.schedule(
|
`save-${this.content}`,
|
async () => {
|
// Ensure latest changes are in IndexedDB
|
if (this.changes.size > 0) {
|
this.cancelBackup();
|
await this.handleBackup();
|
}
|
|
await this.savePosts('', false);
|
},
|
delay
|
);
|
}
|
handleFilterChange(target) {
|
let filter = target.dataset.filter;
|
|
if (filter === 'date' && target.value === 'custom') {
|
target.value = '';
|
this.modals.date.handleOpen();
|
return;
|
}
|
|
if (filter === 'date' && target.value !== '') {
|
this.setFilter('date-filter', target.value);
|
// Clear custom range
|
this.deleteFilter('dateFrom');
|
this.deleteFilter('dateTo');
|
this.checkHideFilters();
|
return;
|
}
|
|
if (filter === 'taxonomies') {
|
filter = `tax_${target.dataset.taxonomy}`;
|
}
|
|
this.setFilter(filter, target.value);
|
}
|
checkHideFilters() {
|
const filters = this.store.filters;
|
const hasActiveFilter = Object.entries(filters).some(([key, value]) => {
|
// Skip internal props
|
if (['content', 'user', 'page'].includes(key)) return false;
|
// Check if differs from default
|
return this.defaults[key] !== value && value !== '' && value !== null;
|
});
|
|
this.ui.buttons.clearFilters.hidden = !hasActiveFilter;
|
}
|
clearAllFilters() {
|
let currentFilters = this.store.filters;
|
this.store.clearFilters();
|
for (let [filter, value] of Object.entries(currentFilters)) {
|
this.cache.remove(filter);
|
this.deleteFilter(filter, value);
|
}
|
this.a11y.announce('All filters cleared');
|
}
|
|
handleCustomDateSelection() {
|
|
// Check if month select was used
|
if (this.ui.modals.date.month && this.ui.modals.date.month.value) {
|
const [year, month] = this.ui.modals.date.month.value.split('-');
|
const firstDay = `${year}-${month}-01`;
|
const lastDay = new Date(year, parseInt(month), 0).getDate();
|
const lastDayFormatted = `${year}-${month}-${String(lastDay).padStart(2, '0')}`;
|
|
this.setFilter('dateFrom', firstDay);
|
this.setFilter('dateTo', lastDayFormatted);
|
|
// Clear the regular date-filter
|
this.deleteFilter('date-filter');
|
|
// Reset month select for next time
|
this.ui.modals.date.month.value = '';
|
}
|
// Otherwise check custom range
|
else if (this.ui.modals.date.start && this.ui.modals.date.start.value && this.ui.modals.date.end && this.ui.modals.date.end.value) {
|
this.setFilter('dateFrom', this.ui.modals.date.start.value);
|
this.setFilter('dateTo', this.ui.modals.date.end.value);
|
|
// Clear the regular date-filter
|
this.deleteFilter('date-filter');
|
|
// Reset inputs for next time
|
this.ui.modals.date.start.value = '';
|
this.ui.modals.date.end.value = '';
|
}
|
|
this.checkHideFilters();
|
}
|
handleViewChange(view) {
|
this.view = view.dataset.view;
|
this.cache.set('view', this.view);
|
this.render();
|
}
|
|
handleClick(e) {
|
// Use matches() instead of closest() where possible (faster)
|
if (e.target.matches('.clear-search')) {
|
this.deleteFilter('search', '');
|
return;
|
}
|
|
const actionButton = e.target.closest('[data-action]');
|
if (actionButton) {
|
e.preventDefault();
|
this.handleActionButton(actionButton);
|
return;
|
}
|
|
if (e.target.matches('.apply-date-filter')) {
|
this.handleCustomDateSelection();
|
this.modals.date.handleClose();
|
return;
|
}
|
|
if (e.target.matches(this.selectors.buttons.create) || e.target.closest(this.selectors.buttons.create)) {
|
this.openCreateModal();
|
}
|
}
|
openCreateModal(){
|
this.forms.registerForm(this.ui.modals.create.form,{
|
cache: false,
|
});
|
this.ui.modals.create.modal.dataset.itemId = window.generateID('new');
|
|
this.modals.create.handleOpen();
|
}
|
handleActionButton(button) {
|
const itemID = button.dataset.id;
|
|
switch (button.dataset.action) {
|
case 'edit':
|
this.openEditModal(itemID);
|
break;
|
case 'delete':
|
if (confirm('Delete this item? This cannot be undone')) {
|
this.updateItem(itemID, 'post_status', 'delete');
|
window.fade(button.closest('.item'), false);
|
this.savePosts(`Permanently deleting ${this.singular}...`).then(()=>{});
|
this.store.delete(itemID);
|
}
|
break;
|
case 'trash':
|
if (this.status === 'trash') {
|
if (confirm('Delete this item? This cannot be undone')) {
|
this.updateItem(itemID, 'post_status', 'delete');
|
window.fade(button.closest('.item'), false);
|
this.savePosts(`Permanently deleting ${this.singular}...`).then(()=>{});
|
this.store.delete(itemID);
|
}
|
} else {
|
this.updateItem(itemID, 'post_status', 'trash');
|
window.fade(button.closest('.item'), false);
|
this.savePosts(`Sending ${this.singular} to trash...`).then(()=>{});
|
}
|
break;
|
case 'bulk-edit':
|
if (this.selected.size > 0) {
|
this.openBulkEditModal();
|
}
|
break;
|
case 'bulk-delete':
|
this.handleBulkDelete();
|
break;
|
case 'refresh':
|
this.store.clearCache();
|
this.store.fetch();
|
break;
|
case 'clear-filters':
|
this.clearAllFilters();
|
break;
|
}
|
}
|
handleBulkDelete() {
|
let isTrash = this.status === 'trash';
|
if (this.selected.size > 0 && confirm(`${isTrash ? 'Permanently delete' : 'Send'} ${this.selected.size} ${this.selected.size === 1 ? this.singular : this.plural}${isTrash ? '' : 'to trash'}?`)) {
|
this.selected.forEach(id => {
|
this.store.delete(id);
|
this.updateItem(id, 'post_status', isTrash ? 'delete' : 'trash');
|
});
|
let title = isTrash
|
? `Permanently deleting ${this.selected.size} ${this.selected.size === 1 ? this.singular : this.plural}`
|
: `Sending ${this.selected.size} ${this.selected.size === 1 ? this.singular : this.plural} to trash`;
|
this.savePosts(title).then(()=>{});
|
this.selectionHandler.clearSelection();
|
}
|
}
|
|
handleInput(e) {
|
e.preventDefault();
|
e.stopPropagation();
|
let query = e.target.value.trim();
|
let key = `${this.content}-search`;
|
|
if (query.length === 0) {
|
this.deleteFilter('search', '');
|
return;
|
}
|
|
// Require minimum 2 characters
|
// if (query.length < 2) {
|
// return;
|
// }
|
|
window.debouncer.schedule(
|
key,
|
() => {
|
this.a11y.announce(`Searching for "${query}"...`);
|
this.store.setFilters({ search: query, page: 1 });
|
},
|
300
|
);
|
}
|
|
handleKeys(e) {
|
if (!this.tabNav) return;
|
|
if (e.key === 'Tab') {
|
e.preventDefault();
|
|
const currentCell = e.target.closest('[data-field]');
|
const currentRow = e.target.closest('tr');
|
|
if (!currentCell || !currentRow) return;
|
|
const fieldName = currentCell.dataset.field;
|
const isShift = e.shiftKey;
|
|
// Find next editable row
|
let targetRow = this.findNextEditableRow(currentRow, isShift);
|
|
// If no target row found, wrap around
|
if (!targetRow) {
|
targetRow = this.wrapToRow(currentRow, isShift);
|
}
|
|
if (targetRow) {
|
this.focusFieldInRow(targetRow, fieldName, isShift);
|
}
|
}
|
}
|
findNextEditableRow(currentRow, goBackward = false) {
|
let row = goBackward ? currentRow.previousElementSibling : currentRow.nextElementSibling;
|
|
// For timeline tables, skip non-editable rows
|
while (row && !this.isEditableRow(row)) {
|
row = goBackward ? row.previousElementSibling : row.nextElementSibling;
|
}
|
|
return row;
|
}
|
|
wrapToRow(currentRow, goBackward = false) {
|
if (this.isTimeline) {
|
// For timeline, stay within the same tbody
|
const tbody = currentRow.closest('tbody');
|
if (!tbody) return null;
|
|
const rows = Array.from(tbody.querySelectorAll('tr'))
|
.filter(row => this.isEditableRow(row));
|
|
return goBackward ? rows[rows.length - 1] : rows[0];
|
} else {
|
// For regular tables, use all rows in tbody
|
if (!this.ui.table.body) return null;
|
|
const rows = Array.from(this.ui.table.body.querySelectorAll('tr'))
|
.filter(row => this.isEditableRow(row));
|
|
return goBackward ? rows[rows.length - 1] : rows[0];
|
}
|
}
|
isEditableRow(row) {
|
// Skip thead/tfoot
|
if (row.closest('thead') || row.closest('tfoot')) {
|
return false;
|
}
|
|
// For timeline, check for specific classes
|
if (this.isTimeline) {
|
return row.classList.contains('shared') || row.classList.contains('timeline-point');
|
}
|
|
// For regular tables, check for data-id
|
return !!row.dataset.itemId;
|
}
|
|
focusFieldInRow(row, fieldName, fromAbove = false) {
|
const targetCell = row.querySelector(`[data-field="${fieldName}"]`);
|
if (!targetCell) return;
|
|
const input = this.findFocusableInput(targetCell);
|
if (input) {
|
input.focus();
|
|
// Select text if it's a text input
|
if (input.select && input.type === 'text') {
|
input.select();
|
}
|
|
// Announce for accessibility
|
const direction = fromAbove ? 'next' : 'previous';
|
this.a11y?.announce(`Moved to ${fieldName} in ${direction} row`);
|
}
|
}
|
|
findFocusableInput(cell) {
|
const selectors = [
|
'input:not([type="hidden"]):not([disabled])',
|
'textarea:not([disabled])',
|
'select:not([disabled])',
|
'button:not([disabled])'
|
];
|
|
for (const selector of selectors) {
|
const element = cell.querySelector(selector);
|
if (element) return element;
|
}
|
|
return null;
|
}
|
|
|
/*******************************************************************
|
MODALS
|
*******************************************************************/
|
openEditModal(itemID) {
|
let item = this.store.get(parseInt(itemID));
|
if (!item) return;
|
this.activeItem = item.id;
|
this.ui.modals.edit.modal.dataset.itemId = itemID;
|
this.ui.modals.edit.modal.dataset.content = this.content;
|
let title;
|
if (Object.hasOwn(item.fields, 'post_title')) {
|
title = item.fields.post_title;
|
} else if (Object.hasOwn(item.fields, 'name')) {
|
title = item.fields.name;
|
}
|
this.ui.modals.edit.h2.textContent = `Editing ${title === '' ? this.singular : title}`;
|
this.ui.modals.edit.form.dataset.formId = `edit-${itemID}`;
|
|
|
this.modals.edit.handleOpen();
|
this.forms.registerForm(this.ui.modals.edit.form, {cache: false,
|
autoUpload: true,});
|
|
|
this.isPopulating = true;
|
this.populate.populate(this.ui.modals.edit.form, item);
|
//For quill/taxonomy selector's async setups
|
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
this.isPopulating = false;
|
});
|
});
|
|
}
|
openBulkEditModal() {
|
window.removeChildren(this.ui.modals.bulkEdit.selected);
|
this.ui.modals.edit.form.reset();
|
|
window.chunkIt(
|
this.selected,
|
(itemId) => {
|
let item = this.store.get(parseInt(itemId));
|
if (!item) return;
|
itemIds.push(item.id);
|
|
return window.jvbTemplates.create('bulkItem', item);
|
},
|
(fragment) => this.ui.modals.bulkEdit.selected.append(fragment)
|
).then(()=>{});
|
let itemIds = Array.from(this.selected).map(id => this.store.get(parseInt(id))).filter(Boolean);
|
|
this.ui.modals.bulkEdit.modal.dataset.itemId = itemIds.join(',');
|
|
if (this.ui.modals.bulkEdit.h2) {
|
this.ui.modals.bulkEdit.h2.textContent = this.selected.size;
|
}
|
this.modals.bulkEdit.handleOpen();
|
|
|
this.forms.registerForm(this.ui.modals.bulkEdit.form, {cache:false});
|
this.isPopulating = true;
|
this.populate.populate(this.ui.modals.edit.form, item);
|
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
this.isPopulating = false;
|
});
|
});
|
}
|
|
/*****************************************************************
|
FIELD HANDLING
|
*****************************************************************/
|
|
async savePosts(title = '', delay = false) {
|
if (this.changes.size > 0) {
|
this.cancelBackup();
|
await this.handleBackup();
|
}
|
let changes = await this.changesStore.getAll();
|
if (changes.length === 0) return;
|
|
// Filter out false positives
|
changes = this.validateChanges(changes);
|
if (changes.length === 0) return;
|
|
if (title === '') {
|
title = `Saving ${changes.length} ${changes.length === 1 ? this.singular : this.plural}`;
|
}
|
|
let allChanges = {};
|
let remove = [];
|
|
changes.forEach(change => {
|
let itemId = change.id;
|
const { id, ...changeWithoutId } = change;
|
allChanges[itemId] = changeWithoutId;
|
|
if (change.post_status && this.shouldRemoveItemUI(change.post_status)) {
|
remove.push(itemId);
|
}
|
});
|
|
if (remove.length > 0) {
|
this.removeItems(remove);
|
}
|
|
let operation = {
|
endpoint: this.endpoint,
|
headers: {
|
'X-Action-Nonce': window.auth.getNonce('dash'),
|
},
|
data: {
|
posts: allChanges,
|
},
|
delay: delay,
|
popup: `Saving changes`,
|
title: title
|
};
|
this.queue.addToQueue(operation);
|
}
|
|
/**
|
* Compare pending changes against the store, removing unchanged fields.
|
* Returns cleaned array (may be empty if nothing actually changed).
|
*/
|
validateChanges(changes) {
|
return changes.reduce((valid, change) => {
|
const { id, content, ...fields } = change;
|
const stored = this.store.get(id);
|
|
if (!stored) {
|
valid.push(change);
|
return valid;
|
}
|
|
const realChanges = { id, content };
|
let hasRealChange = false;
|
|
for (const [name, value] of Object.entries(fields)) {
|
const storedValue = stored.fields?.[name] ?? stored[name];
|
const diff = window.getDifferences.map(storedValue, value);
|
|
if (diff !== null) {
|
realChanges[name] = value;
|
hasRealChange = true;
|
}
|
}
|
|
if (hasRealChange) {
|
valid.push(realChanges);
|
} else {
|
this.changes.delete(id);
|
this.changesStore.delete(id);
|
}
|
|
return valid;
|
}, []);
|
}
|
|
|
setBulkStatus(status) {
|
if (!['publish', 'draft', 'trash', 'delete'].includes(status)) return;
|
let ids = [];
|
this.selected.forEach(itemID => {
|
ids.push(itemID);
|
this.updateItem(itemID, 'post_status', status);
|
});
|
let title;
|
switch (status) {
|
case 'delete':
|
title = 'Deleting';
|
break;
|
default:
|
title = window.uppercaseFirst(status)+'ing';
|
}
|
if (this.shouldRemoveItemUI(status)) {
|
this.removeItems(ids);
|
}
|
this.selectionHandler.clearSelection();
|
|
this.savePosts(`${title} ${ids.length} ${ids.length === 1 ? this.singular : this.plural}...`).then(()=>{});
|
|
}
|
/***************************************************************
|
VIEW
|
***************************************************************/
|
render() {
|
const items = this.store.getFiltered();
|
if (items.length === 0) {
|
this.renderEmpty();
|
return;
|
}
|
|
switch (this.view) {
|
case 'grid':
|
this.renderGrid(items);
|
break;
|
case 'table':
|
this.renderTable(items).then(()=>{});
|
break;
|
case 'list':
|
this.renderList(items);
|
break;
|
}
|
this.updateUI();
|
}
|
updateUI() {
|
if (this.ui.bulk.action) {
|
let options = false;
|
let hasEdit = this.ui.bulk.action.querySelector('[value="edit"]');
|
let currentStatus = this.status;
|
|
if (currentStatus === 'trash' && hasEdit) {
|
window.removeChildren(this.ui.bulk.action);
|
options = window.jvbTemplates.create('trashOptions');
|
} else if (currentStatus !== 'trash' && !hasEdit) {
|
window.removeChildren(this.ui.bulk.action);
|
options = window.jvbTemplates.create('notTrashOptions');
|
}
|
if (options) {
|
options.querySelectorAll('option').forEach((option, index)=> {
|
if (index === 0) option.checked = true;
|
this.ui.bulk.action.append(option);
|
});
|
}
|
this.ui.bulk.action.value = '';
|
}
|
if (this.selected.size > 0) {
|
this.selectionHandler.updateSelectionUI();
|
}
|
}
|
|
renderEmpty() {
|
this.toggleTable(false);
|
window.removeChildren(this.ui.grid);
|
const empty = window.jvbTemplates.create('emptyState');
|
if (empty) {
|
this.ui.grid.append(empty);
|
this.a11y.announceItems(0,false,false);
|
}
|
}
|
|
toggleTable(on = true) {
|
if (this.ui.table.selectedColumns) this.ui.table.selectedColumns.hidden = !on;
|
|
if (on && !this.ui.table.form) {
|
let table = window.jvbTemplates.create('contentTable');
|
this.container.append(table);
|
this.ui.table = window.uiFromSelectors(this.selectors.table);
|
this.ui.table.columns = this.container.querySelectorAll(this.selectors.table.columns);
|
}
|
|
if (this.ui.table.form) {
|
this.ui.table.form.hidden = !on;
|
if (!on){
|
this.forms.clearForm(this.ui.table.form.dataset.formId)
|
}
|
if (this.ui.table.body) {
|
window.removeChildren(this.ui.table.body);
|
}
|
}
|
this.keyHandler = this.handleKeys.bind(this);
|
if (on) {
|
document.addEventListener('keydown', this.keyHandler);
|
} else {
|
document.removeEventListener('keydown', this.keyHandler);
|
}
|
|
}
|
|
renderGrid(items) {
|
window.removeChildren(this.ui.grid);
|
this.toggleTable(false);
|
|
this.ui.grid.classList.remove('list-view');
|
this.ui.grid.classList.add('grid-view');
|
|
window.chunkIt(
|
items,
|
(item) => this.renderGridItem(item),
|
(fragment) => this.ui.grid.append(fragment)
|
).then(()=>{});
|
}
|
|
renderList(items) {
|
window.removeChildren(this.ui.grid);
|
this.toggleTable(false);
|
|
this.ui.grid.classList.remove('grid-view');
|
this.ui.grid.classList.add('list-view');
|
window.chunkIt(
|
items,
|
(item) => this.renderListItem(item),
|
(fragment) => this.ui.grid.append(fragment)
|
).then(()=>{});
|
}
|
|
async renderTable(items) {
|
this.toggleTable();
|
window.removeChildren(this.ui.grid);
|
|
await window.chunkIt(
|
items,
|
(item) => this.renderTableItem(item),
|
(fragment) => {
|
if (this.ui.table.body) {
|
this.ui.table.body.append(fragment);
|
} else {
|
this.ui.table.table.insertBefore(fragment, this.ui.table.foot);
|
}
|
},
|
5
|
);
|
|
requestAnimationFrame(() => {
|
window.jvbSelector?.scanExistingFields(this.ui.table.table);
|
});
|
}
|
|
/***************************************************************
|
RENDER HELPERS
|
***************************************************************/
|
renderGridItem(item) {
|
let gridItem = window.jvbTemplates.create('gridView', item);
|
this.items.set(item.id, gridItem);
|
return gridItem;
|
}
|
|
renderListItem(item) {
|
let listItem = window.jvbTemplates.create('listView', item);
|
this.items.set(item.id, listItem);
|
return listItem;
|
}
|
|
renderTableItem(item) {
|
let tableItem = window.jvbTemplates.create('tableView', item);
|
this.items.set(item.id, tableItem);
|
return tableItem;
|
}
|
|
toggleColumn(column, show) {
|
this.ui.table.table.querySelectorAll(`.${column}`).forEach(el =>{
|
el.hidden = !show;
|
});
|
}
|
/***************************************************************
|
UPLOAD GROUP SUPPORT
|
Handles:
|
- immediate UI feedback once the uploaded groups are sent to server
|
***************************************************************/
|
handleGroupsUploaded(data) {
|
const { posts, fieldId } = data;
|
let uploader = window.jvbUploads;
|
let field = uploader.fields.get(fieldId);
|
|
let added = [];
|
posts.forEach(post => {
|
const placeholderPost = {
|
id: post.groupId,
|
title: post.fields.post_title || `New ${this.singular}`,
|
status: 'draft',
|
date: new Date().toISOString(),
|
modified: new Date().toISOString(),
|
thumbnail: null,
|
icon: this.content,
|
taxonomies: {},
|
fields: post.fields,
|
images: {},
|
};
|
|
post.images.forEach((uploadId, index) => {
|
let id = uploadId['upload_id'];
|
if (index === 0) {
|
placeholderPost.fields['post_thumbnail'] = uploadId;
|
}
|
let upload = uploader.stores.uploads.get(id);
|
if (upload) {
|
placeholderPost.images[id] = {
|
'image-alt-text': '',
|
'image-caption': '',
|
'image-title': upload.fields.originalName,
|
medium: uploader.createPreviewUrl(uploader.formatFile(upload))
|
};
|
}
|
|
});
|
//
|
// // Add to store (won't persist since it's a fake ID)
|
// this.store.data.set(post.groupId, placeholderPost);
|
//
|
//
|
// // Render immediately
|
// let element;
|
// switch (this.view) {
|
// case 'grid':
|
// element = this.renderGridItem(placeholderPost);
|
// this.ui.grid.prepend(element);
|
// break;
|
// case 'list':
|
// element = this.renderListItem(placeholderPost);
|
// this.ui.grid.prepend(element);
|
// break;
|
// case 'table':
|
// element = this.renderTableItem(placeholderPost);
|
// if (this.ui.table.body) {
|
// this.ui.table.body.prepend(element);
|
// }
|
// break;
|
// }
|
// element.classList.add('uploading');
|
added.push(placeholderPost);
|
});
|
this.store.saveMany(added).then(() => this.render());
|
|
|
this.a11y.announce(`${posts.length} ${posts.length === 1 ? this.singular : this.plural} created. Waiting for server confirmation...`);
|
}
|
|
handleGroupMappings(mappings) {
|
// mappings = { "group_abc123": 456, "group_def456": 789 }
|
|
for (const [groupId, postId] of Object.entries(mappings)) {
|
// Get any pending changes for this temp item
|
let changes = {};
|
if (this.changes.has(groupId)) {
|
changes = this.changes.get(groupId);
|
this.changes.delete(groupId);
|
}
|
let storedChanges = this.changesStore.get(groupId)??{};
|
if (changes.size > 0 || storedChanges.size > 0) {
|
changes = window.deepMerge(storedChanges, changes);
|
this.changes.set(postId, changes);
|
this.scheduleBackup();
|
}
|
}
|
}
|
/***************************************************************
|
UTILITY
|
***************************************************************/
|
shouldRemoveItemUI(newStatus) {
|
return (this.status === 'all' && !['publish', 'draft'].includes(newStatus))
|
|| newStatus !== this.store.filters.status;
|
}
|
removeItems(items) {
|
items.forEach(itemId => {
|
if (this.items.has(itemId)) {
|
let item = this.items.get(itemId);
|
if (item) window.fade(item, false);
|
}
|
});
|
}
|
|
setFilters(filters) {
|
for (let [key, value] of Object.entries(filters)) {
|
if (!this.allowedFilters.includes(key)) {
|
delete filters[key];
|
continue;
|
}
|
this.cache.set(key, value);
|
|
let el = this.findFilterEl(key);
|
this.setElValue(el, value);
|
}
|
this.store.setFilters(filters);
|
}
|
setFilter(name, value) {
|
if (!this.allowedFilters.includes(name)) return;
|
this.cache.set(name, value);
|
|
if (name === 'status') this.status = value;
|
if (name === 'orderby') this.orderby = value;
|
if (name === 'order') this.order = value;
|
|
let el = this.findFilterEl(name, value);
|
this.setElValue(el, value);
|
this.store.setFilter(name, value);
|
}
|
|
deleteFilter(name, value) {
|
if (!this.allowedFilters.includes(name)) return;
|
if (Object.hasOwn(this.defaults, name)) {
|
this.setFilter(name, this.defaults[name]);
|
return;
|
}
|
let el = this.findFilterEl(name, value);
|
this.setElValue(el, false);
|
this.cache.remove(name);
|
this.setFilter(name, '');
|
}
|
setElValue(element, value) {
|
if (!element) return;
|
if (!value) {
|
if (['SELECT','TEXTAREA'].includes(element.tagName)) element.value = '';
|
if (['text', 'search'].includes(element.type)) element.value = '';
|
if (element.type === 'radio') element.checked = false;
|
return;
|
}
|
|
if (['SELECT','TEXTAREA'].includes(element.tagName)) element.value = value;
|
if (['text', 'search'].includes(element.type)) element.value = value;
|
if (element.type === 'radio') element.checked = true;
|
}
|
findFilterEl(name, value) {
|
//Handle exceptions first (custom date elements)
|
if (['date-filter', 'dateFrom', 'dateTo'].includes(name)) {
|
switch (name) {
|
case 'date-filter':
|
name = 'month';
|
break;
|
case 'dateFrom':
|
name = 'start';
|
break;
|
case 'dateTo':
|
name = 'end';
|
break;
|
}
|
return this.ui.modals.date[name];
|
}
|
// Handle taxonomy filters
|
if (name.includes('tax_')) {
|
const taxonomy = name.replace('tax_', '');
|
const element = this.ui.filters.taxonomies?.[taxonomy];
|
if (element) {
|
return element;
|
}
|
console.warn('Taxonomy filter element not found:', taxonomy);
|
return null;
|
}
|
|
if (!Object.hasOwn(this.ui.filters, name)) {
|
console.warn('Filter el not found: ', name);
|
return false;
|
}
|
|
let el = this.ui.filters[name];
|
if (typeof el === 'object') {
|
if (!Object.hasOwn(this.ui.filters[name], value)) {
|
return false;
|
}
|
el = this.ui.filters[name][value];
|
}
|
return el;
|
}
|
/***************************************************************
|
CLEANUP
|
***************************************************************/
|
resetForm(form) {
|
// Clear text inputs, textareas
|
form.querySelectorAll('input[type="hidden"], input[type="text"], input[type="number"], input[type="email"], input[type="url"], textarea').forEach(input => {
|
input.value = '';
|
});
|
|
// Uncheck checkboxes and radios
|
form.querySelectorAll('input[type="checkbox"], input[type="radio"]').forEach(input => {
|
input.checked = false;
|
});
|
|
// Reset selects to first option
|
form.querySelectorAll('select').forEach(select => {
|
select.selectedIndex = 0;
|
});
|
|
// Clear any selected items displays
|
form.querySelectorAll('.selected-items').forEach(container => {
|
window.removeChildren(container);
|
});
|
|
// Clear upload previews
|
form.querySelectorAll('.item-grid.preview').forEach(grid => {
|
window.removeChildren(grid);
|
});
|
}
|
destroy() {
|
window.debouncer.cancel(`changes-${this.content}`);
|
if (this.changes.size > 0) {
|
this.changesStore.saveMany(this.changes).then(()=>{});
|
this.changes.clear();
|
}
|
if (this.timelineSortables) {
|
this.timelineSortables.forEach(sortable => sortable.destroy());
|
this.timelineSortables = [];
|
}
|
for (let [name, modal] of Object.entries(this.ui.modals)) {
|
if (modal.form) {
|
modal.form.removeEventListener('submit', this.submitHandler);
|
}
|
}
|
document.removeEventListener('click', this.clickHandler);
|
document.removeEventListener('change', this.changeHandler);
|
if (this.ui.filters.search) {
|
this.ui.filters.search.removeEventListener('input', this.handleInput);
|
}
|
}
|
}
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
window.auth.subscribe((event) => {
|
if (event === 'auth-loaded') {
|
let container = document.querySelector('[data-content]');
|
if (container && !Object.hasOwn(container.dataset, 'ignore')) {
|
window.crudManager = new CRUDManager({
|
content: container.dataset.content,
|
});
|
}
|
}
|
});
|
});
|