From 2127b1bdd73ecd2423e443992da4b442f5a3c1a3 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Wed, 04 Feb 2026 21:19:25 +0000
Subject: [PATCH] =Major overhaul of MetaManager.php -> Meta.php and RestRouteManager.php -> Rest.php. Seems to work for JakeVan
---
assets/js/concise/CRUD.js | 1203 ++++++++++++++++++++++++++++++--------------------------
1 files changed, 650 insertions(+), 553 deletions(-)
diff --git a/assets/js/concise/CRUD.js b/assets/js/concise/CRUD.js
index 5815706..d97155b 100644
--- a/assets/js/concise/CRUD.js
+++ b/assets/js/concise/CRUD.js
@@ -10,15 +10,16 @@
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.items = {
- list: new Map(),
- grid: new Map(),
- table: new Map()
- }; //DOM references
+ this.isPopulating = false;
+
+ //Track Changes
+ this.changes = new Map();
+ this.items = new Map();
this.init();
}
@@ -26,6 +27,7 @@
init() {
this.initElements();
this.initListeners();
+ this.defineTemplates();
let cached = this.initSettings();
this.initStore(cached);
this.checkHideFilters();
@@ -34,6 +36,220 @@
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) {
+ if (data?.fields?.post_thumbnail) {
+ const thumbnail = data.images[data.fields.post_thumbnail] ?? {};
+ 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 = {
@@ -147,14 +363,20 @@
case 'modal-close':
const formId = this.ui.modals[name].form.dataset.formId;
if (formId) {
- this.formController.cleanupForm(formId);
+ this.forms.clearForm(formId);
}
- this.ui.modals[name].form.reset();
+ 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':
@@ -170,29 +392,39 @@
... this.defaults,
...cached
};
- const store = window.jvbStore.register(
+
+
+ const stores = window.jvbStore.register(
this.content,
- {
- storeName: this.content,
- keyPath: 'id',
- endpoint: this.endpoint??'content', //for taxonomy stores
- headers: {
- 'action_nonce': window.auth.getNonce('dash'),
+ [
+ {
+ storeName: this.content,
+ keyPath: 'id',
+ endpoint: this.endpoint??'content', //for taxonomy stores
+ headers: {
+ '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'},
+ ],
+ filters: filters,
+ ignore: ['content', 'user'],
+ TTL: 60 * 60 * 1000, //1 hour cache
+ showLoading: true,
},
- indexes: [
- {name: 'id', keyPath: 'id'},
- { name: 'status', keyPath: 'status'},
- { name: 'date', keyPath: 'date'},
- { name: 'modified', keyPath: 'modified'},
- { name: 'title', keyPath: 'title'}
- ],
- filters: filters,
- ignore: ['content', 'user'],
- TTL: 60 * 60 * 1000, //1 hour cache
- showLoading: true,
- }
+ {
+ storeName: 'changes',
+ keyPath: 'id'
+ }
+ ]
);
- this.store = store[this.content];
+
+ this.changesStore = stores['changes'];
+ this.store = stores[this.content];
this.store.subscribe((event, data) => {
switch (event) {
@@ -201,18 +433,35 @@
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: '.crud #select-all',
+ checkbox: '#select-all',
label: '.bulk-select label',
span: '.bulk-select label span'
},
wrapper: {
wrapper: '.wrap'
+ },
+ item: {
+ idAttribute: 'itemId'
}
});
this.selectionHandler.subscribe((event, data) => {
@@ -222,16 +471,16 @@
this.ui.bulk.count.textContent = `${this.selected.size} ${this.plural} selected`;
});
- this.formController = new window.jvbForm();
+ this.forms = window.jvbForm;
- this.formController.subscribe((event, data) => {
- switch(event) {
- case 'form-submit':
- case 'form-autosave':
- this.handleFormChange(event,data);
- break;
- }
- });
+ // this.forms.subscribe((event, data) => {
+ // switch(event) {
+ // case 'form-submit':
+ // case 'form-autosave':
+ // // this.handleFormChange(event,data);
+ // break;
+ // }
+ // });
this.queue.subscribe((event, data) => {
if (['image_upload', 'video_upload', 'document_upload'].includes(data.type)
@@ -239,6 +488,39 @@
&& data.status === 'completed') {
this.store.clearCache();
}
+ if (event === 'operation-status'
+ && data.status === 'completed'
+ && data.endpoint === 'uploads/groups') {
+
+ console.log('Cleared local cache. Refresh to see changes');
+ this.store.clearCache();
+ }
+ if (event === 'operation-status'
+ && data.status === 'completed'
+ && data.type === 'content_update') {
+ console.log('Cleared local cache. Refresh to see changes');
+ this.store.clearCache();
+
+ // Check for result data (from ContentExecutor)
+ if (!data.result || !data.result.posts) {
+ console.warn('Content update completed but no result.posts', data);
+ return;
+ }
+
+ // Get successfully processed post IDs
+ const successfulIds = Object.keys(data.result.posts).filter(id => {
+ return data.result.posts[id]?.success === true;
+ });
+
+ if (successfulIds.length === 0) {
+ return;
+ }
+
+ // Clear from both persistent and in-memory storage
+ this.changesStore.deleteMany(successfulIds);
+ successfulIds.forEach(id => this.changes.delete(id));
+ }
+
});
}
@@ -326,53 +608,74 @@
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;
+ let title = `Saving changes for multiple ${this.plural}`;
+ if (modal.classList.contains('edit')) {
+ title = 'Saving your edits...';
+ } else if (modal.classList.contains('create')) {
+ title = `Creating your new ${this.singular}`;
+ }
+ this.scheduleSave(0);
}
handleChange(e) {
- const isSearch = window.targetCheck(e, this.selectors.filters.search);
- if (isSearch) {
- return;
- }
- const bulkAction = window.targetCheck(e, this.selectors.bulk.action);
- if (bulkAction) {
- this.handleBulkAction(bulkAction);
+ // 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;
}
- let filter = window.targetCheck(e, '[data-filter]');
- if (filter) {
- this.handleFilterChange(filter);
+ if (isView) {
+ this.items.clear();
+ this.handleViewChange(e.target);
return;
}
- let view = window.targetCheck(e, 'input[data-view]');
- if (view) {
- this.handleViewChange(view);
+ if (isBulkAction) {
+ this.handleBulkAction(e.target);
return;
}
+ if (isFilter) {
+ this.handleFilterChange(e.target);
+ return;
+ }
+
+ // Table-specific handlers
if (this.view === 'table') {
- let target = window.targetCheck(e, '[data-id]');
- if (target) {
- this.handleTableChange(e);
- return;
- }
-
- let multiSelect = window.targetCheck(e, 'details.multi-select');
- if (multiSelect) {
+ if (e.target.matches('details.multi-select')) {
this.toggleColumn(e.target.id, e.target.checked);
return;
}
- let tabNav = window.targetCheck(e, this.selectors.table.nav);
- if (tabNav) {
- this.tabNav = tabNav.checked;
- this.cache.set('tabNav', tabNav.checked ? 'vertical' : 'horizontal');
+ if (e.target.matches(this.selectors.table.nav)) {
+ this.tabNav = e.target.checked;
+ this.cache.set('tabNav', e.target.checked ? 'vertical' : 'horizontal');
}
}
}
@@ -411,11 +714,8 @@
handleBulkTaxonomy(result) {
if (!result.termIds.length || !this.selected.size) return;
- const changes = {};
- const taxonomyField = `tax_${result.taxonomy}`;
-
this.selected.forEach(itemID => {
- const item = this.store.get(parseInt(itemID));
+ const item = this.store.get(itemID);
if (!item) return;
// Merge existing terms with new ones
@@ -423,21 +723,82 @@
const existingIds = existingTerms.map(t => t.id);
const newIds = [...new Set([...existingIds, ...result.termIds])];
- changes[itemID] = {
- [taxonomyField]: newIds.join(','),
- content: this.content
- };
+ this.updateItem(itemID, result.taxonomy, newIds);
});
- if (Object.keys(changes).length > 0) {
- this.savePosts(
- changes,
- `Adding ${result.terms.length} ${result.taxonomy} to ${this.selected.size} ${this.plural}...`
- );
- }
+ 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;
+ item.dataset.itemId.split(',').forEach(itemId => {
+ let field = this.forms.getField(e.target);
+ if (['repeater', 'tag-list'].includes(field.dataset.fieldType)) {
+ return;
+ }
+ let name = field.dataset.field;
+ let value = this.forms.getFieldValue(e.target);
+ this.updateItem(itemId, name, value);
+ });
+ }
+ updateItem(itemId, name, value) {
+ if (!this.changes.has(itemId)) {
+ this.changes.set(itemId, { id: itemId, content: this.content });
+ }
+ this.changes.get(itemId)[name] = value;
+
+ this.scheduleBackup();
+ 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;
@@ -521,40 +882,67 @@
this.cache.set('view', this.view);
this.render();
}
+
handleClick(e) {
- let clearSearch = window.targetCheck(e, '.clear-search');
- if (clearSearch) {
+ // Use matches() instead of closest() where possible (faster)
+ if (e.target.matches('.clear-search')) {
this.deleteFilter('search', '');
+ return;
}
- let actionButton = window.targetCheck(e, '[data-action]');
+
+ const actionButton = e.target.closest('[data-action]');
if (actionButton) {
e.preventDefault();
- let itemID = actionButton.dataset.id;
+ this.handleActionButton(actionButton);
+ return;
+ }
- switch (actionButton.dataset.action) {
+ if (e.target.matches('.apply-date-filter')) {
+ this.handleCustomDateSelection();
+ this.modals.date.handleClose();
+ return;
+ }
+
+ if (e.target.matches(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')) {
- let changes = {};
- changes[itemID] = {
- 'post_status': 'delete',
- 'content': this.content
- };
- window.fade(actionButton.closest('.item'), false);
- this.savePosts(changes, `Sending ${this.singular} to trash...`);
+ 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':
- let changes = {};
- changes[itemID] = {
- 'post_status': 'trash',
- 'content': this.content
- };
- window.fade(actionButton.closest('.item'), false);
- this.savePosts(changes, `Sending ${this.singular} to 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) {
@@ -562,10 +950,7 @@
}
break;
case 'bulk-delete':
- if (this.selected.size > 0 && confirm(`Delete ${this.selected.size} items?`)) {
- this.selected.forEach(id => this.store.delete(id));
- this.selectionHandler.clearSelection();
- }
+ this.handleBulkDelete();
break;
case 'refresh':
this.store.clearCache();
@@ -576,22 +961,21 @@
break;
}
}
-
- const applyDate = window.targetCheck(e, '.apply-date-filter');
- if (applyDate) {
- this.handleCustomDateSelection();
- this.modals.date.handleClose();
- return;
+ 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();
+ }
}
- const createButton = window.targetCheck(e, this.selectors.buttons.create);
- if (createButton) {
- this.ui.modals.create.form.reset();
- this.formController.registerForm(this.ui.modals.create.form);
- this.modals.create.handleOpen();
- }
- }
-
handleInput(e) {
e.preventDefault();
e.stopPropagation();
@@ -688,7 +1072,7 @@
}
// For regular tables, check for data-id
- return !!row.dataset.id;
+ return !!row.dataset.itemId;
}
focusFieldInRow(row, fieldName, fromAbove = false) {
@@ -726,82 +1110,6 @@
return null;
}
- toggleTimelineListeners(on = true) {
- if (!this.isTimeline || this.view !== 'table') return;
-
- // Cleanup existing instances
- if (this.timelineSortables) {
- this.timelineSortables.forEach(sortable => sortable.destroy());
- this.timelineSortables = [];
- }
-
- if (!on) return;
-
- // Initialize sortable for each tbody
- this.timelineSortables = [];
- const tbodies = this.ui.table.form.querySelectorAll('tbody.item');
-
- tbodies.forEach(tbody => {
- const sortable = new Sortable(tbody, {
- animation: 150,
- handle: '.drag-handle',
- draggable: 'tr.timeline-point',
- ghostClass: 'sortable-ghost',
- chosenClass: 'sortable-chosen',
- dragClass: 'sortable-drag',
-
- // Prevent dragging between different tbodies
- group: {
- name: `timeline-${tbody.dataset.id}`,
- pull: false,
- put: false
- },
-
- onEnd: (evt) => this.handleTimelineReorder(evt)
- });
-
- this.timelineSortables.push(sortable);
- });
- }
-
- handleTimelineReorder(evt) {
- const { item: draggedRow, oldIndex, newIndex, from: tbody } = evt;
-
- // No change
- if (oldIndex === newIndex) return;
-
- const itemID = parseInt(tbody.dataset.id);
- const item = this.store.get(itemID);
- if (!item?.fields?.timeline) return;
-
- // Get current order of image IDs from DOM
- const newOrder = Array.from(tbody.querySelectorAll('tr.timeline-point'))
- .map(row => row.dataset.imageId)
- .filter(Boolean);
-
- // Rebuild timeline object in new order
- const reorderedTimeline = {};
- newOrder.forEach((imgId, index) => {
- if (item.fields.timeline[imgId]) {
- reorderedTimeline[imgId] = {
- ...item.fields.timeline[imgId],
- order: index
- };
- }
- });
-
- item.fields.timeline = reorderedTimeline;
-
- // Save to store and server
- this.store.save(item);
- this.savePosts(
- { [itemID]: { timeline: reorderedTimeline, content: this.content } },
- 'Reordering timeline...',
- true
- );
-
- this.a11y?.announce(`Moved to position ${newIndex + 1}`);
- }
/*******************************************************************
MODALS
@@ -815,108 +1123,80 @@
this.ui.modals.edit.h2.textContent = `Editing ${item.fields.post_title === '' ? this.singular : item.fields.post_title}`;
this.ui.modals.edit.form.dataset.formId = `edit-${itemID}`;
- this.ui.modals.edit.form.reset();
- new window.jvbPopulate(this.ui.modals.edit.form, item);
- this.formController.registerForm(this.ui.modals.edit.form);
+ this.forms.registerForm(this.ui.modals.edit.form, {cache: false});
+
+ this.isPopulating = true;
+ this.populate.populate(this.ui.modals.edit.form, item);
+ this.isPopulating = false;
this.modals.edit.handleOpen();
}
openBulkEditModal() {
window.removeChildren(this.ui.modals.bulkEdit.selected);
this.ui.modals.edit.form.reset();
- this.modals.bulkEdit.handleOpen();
- this.selected.forEach(itemId => {
- let template = window.getTemplate('bulkItem');
- if (!template) return;
- let item = this.store.get(parseInt(itemId));
- if (!item) return;
- let [checkbox, img, label] = [template.querySelector('input'), template.querySelector('img'), template.querySelector('label')];
- if (checkbox) {
- checkbox.id = `bulk_${item.id}`;
- checkbox.value = item.id;
- checkbox.checked = true;
- checkbox.name = 'selected[]';
- }
- let thumbnail = item.images[item.fields.post_thumbnail]??{};
- if (img) {
- img.src = thumbnail.medium??'';
- img.alt = thumbnail.alt??''
- }
+ window.chunkIt(
+ this.selected,
+ (itemId) => {
+ let item = this.store.get(parseInt(itemId));
+ if (!item) return;
+ itemIds.push(item.id);
- label.title = item.fields['post_title']??'';
- this.ui.modals.bulkEdit.selected.append(template);
- });
+ 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.formController.registerForm(this.ui.modals.bulkEdit.form);
+ 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);
+ this.isPopulating = false;
}
/*****************************************************************
FIELD HANDLING
*****************************************************************/
- handleFormChange(event, data) {
- let title = data.fullData.post_title;
- let changes = (Object.hasOwn(data, 'changes')) ? data.changes : data.fullData;
- let theChanges = {};
- if (this.isTimeline) {
- theChanges[this.activeItem] = changes;
- if (changes.post_status && this.shouldRemoveItemUI(changes.post_status)) {
- this.removeItems([this.activeItem]);
- }
- this.savePosts(theChanges, title, event === 'form-submit');
- return;
+ async savePosts(title = '', delay = false) {
+ if (this.changes.size > 0) {
+ this.cancelBackup();
+ await this.handleBackup();
+ }
+ const changes = await this.changesStore.getAll();
+ console.log('Saving Changes: ', changes);
+ if (changes.length === 0) return;
+
+ if (title === '') {
+ title = `Saving ${changes.length} ${changes.length === 1 ? this.singular : this.plural}`;
}
+ let allChanges = {};
let remove = [];
- let el = data.config.element;
- if (el === this.ui.modals.edit.form) {
- theChanges[this.activeItem] = changes;
- title = `Saving ${title} Changes`;
- if (changes.post_status && this.shouldRemoveItemUI(changes.post_status)) {
- remove.push(this.activeItem);
+
+ changes.forEach(change => {
+ let itemId = change.id;
+
+ // Create a new object without the id field (don't mutate original!)
+ const { id, ...changeWithoutId } = change;
+ allChanges[itemId] = changeWithoutId;
+
+ if (change.post_status && this.shouldRemoveItemUI(change.post_status)) {
+ remove.push(itemId);
}
- } else if (el === this.ui.modals.bulkEdit.form) {
- let num = 0;
- el.querySelectorAll('.selected input:checked').forEach(selected => {
- theChanges[selected.value] = changes;
- if (changes.post_status && this.shouldRemoveItemUI(changes.post_status)) {
- remove.push(selected.value);
- }
- num++;
- });
- title = `Updating ${num} ${this.plural} Changes`;
- } else if (el === this.ui.modals.create.form) {
- if (event === 'form-submit') {
- theChanges[el.dataset.formId] = changes;
- title = `Saving ${title} Changes`;
- }
- }
+ });
+
if (remove.length > 0) {
this.removeItems(remove);
}
- this.selectionHandler.clearSelection();
- if (Object.keys(theChanges).length === 0) {
- return;
- }
- this.savePosts(theChanges, title, event === 'form-submit');
- }
- savePosts(changes, title, delay = false) {
- if (Object.keys(changes).length === 0) return;
-
- let validChanges = {};
- for (let itemId in changes) {
- // Validate ID exists and is not null/undefined
- if (!itemId || itemId === 'null' || itemId === 'undefined') {
- console.warn('Skipping save for invalid ID:', itemId);
- continue;
- }
- if (!changes[itemId]['content']) changes[itemId]['content'] = this.content;
-
- validChanges[itemId] = changes[itemId];
- }
let operation = {
endpoint: this.endpoint,
@@ -924,7 +1204,7 @@
'action_nonce': window.auth.getNonce('dash'),
},
data: {
- posts: validChanges,
+ posts: allChanges,
},
delay: delay,
popup: `Saving changes`,
@@ -933,59 +1213,13 @@
this.queue.addToQueue(operation);
}
- handleTableChange(e) {
- const container = this.isTimeline
- ? e.target.closest('tbody[data-id]')
- : e.target.closest('tr[data-id]');
- if (!container) return;
- const input = e.target;
- const field = input.closest('[data-field]')?.dataset.field;
- if (!field) return;
-
- const itemID = parseInt(container.dataset.id);
- const item = this.store.get(itemID);
- if (!item) return;
-
- const value = this.getInputValue(input);
-
- // Timeline-specific: check if it's a point or shared value
- if (this.isTimeline) {
- const timelinePoint = input.closest('tr.timeline-point');
- if (timelinePoint) {
- const imgID = timelinePoint.dataset.imageId;
- item.fields.timeline ??= {};
- item.fields.timeline[imgID] ??= {};
- item.fields.timeline[imgID][field] = value;
- } else {
- item.fields[field] = value;
- }
- } else {
- item.fields[field] = value;
- }
-
- this.store.save(item);
- this.savePosts({ [itemID]: item.fields }, `Saving changes...`, true);
- }
-
- getInputValue(input) {
- if (input.type === 'checkbox') {
- return input.checked ? (input.value || '1') : '';
- }
- if (input.type === 'radio') {
- return input.checked ? input.value : null;
- }
- return input.value;
- }
setBulkStatus(status) {
if (!['publish', 'draft', 'trash', 'delete'].includes(status)) return;
-
- let changes = {};
+ let ids = [];
this.selected.forEach(itemID => {
- changes[itemID] = {
- post_status: status,
- content: this.content
- };
+ ids.push(itemID);
+ this.updateItem(itemID, 'post_status', status);
});
let title;
switch (status) {
@@ -996,12 +1230,12 @@
title = window.uppercaseFirst(status)+'ing';
}
if (this.shouldRemoveItemUI(status)) {
- this.removeItems(Object.keys(changes));
+ this.removeItems(ids);
}
this.selectionHandler.clearSelection();
- if (Object.keys(changes).length !== 0) {
- this.savePosts(changes, `${title} ${Object.keys(changes).length} ${this.plural}...`);
- }
+
+ this.savePosts(`${title} ${ids.length} ${ids.length === 1 ? this.singular : this.plural}...`).then(()=>{});
+
}
/***************************************************************
VIEW
@@ -1018,7 +1252,7 @@
this.renderGrid(items);
break;
case 'table':
- this.renderTable(items);
+ this.renderTable(items).then(()=>{});
break;
case 'list':
this.renderList(items);
@@ -1029,13 +1263,15 @@
updateUI() {
if (this.ui.bulk.action) {
let options = false;
- let hasEdit =this.ui.bulk.action.querySelector('[value="edit"]');
- if (this.status === 'trash' && hasEdit) {
+ 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.getTemplate('trashOptions');
- } else if (this.status !== 'trash' && !hasEdit) {
+ options = window.jvbTemplates.create('trashOptions');
+ } else if (currentStatus !== 'trash' && !hasEdit) {
window.removeChildren(this.ui.bulk.action);
- options = window.getTemplate('notTrashOptions');
+ options = window.jvbTemplates.create('notTrashOptions');
}
if (options) {
options.querySelectorAll('option').forEach((option, index)=> {
@@ -1053,7 +1289,7 @@
renderEmpty() {
this.toggleTable(false);
window.removeChildren(this.ui.grid);
- const empty = window.getTemplate('emptyState');
+ const empty = window.jvbTemplates.create('emptyState');
if (empty) {
this.ui.grid.append(empty);
this.a11y.announceItems(0,false,false);
@@ -1064,7 +1300,7 @@
if (this.ui.table.selectedColumns) this.ui.table.selectedColumns.hidden = !on;
if (on && !this.ui.table.form) {
- let table = window.getTemplate('contentTable');
+ 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);
@@ -1072,14 +1308,8 @@
if (this.ui.table.form) {
this.ui.table.form.hidden = !on;
- if (on) {
- this.formController.registerForm(this.ui.table.form, {
- autosave: false,
- formStatus: false,
- isTable: true
- });
- } else {
- this.formController.cleanupForm(this.ui.table.form.dataset.formId)
+ if (!on){
+ this.forms.clearForm(this.ui.table.form.dataset.formId)
}
if (this.ui.table.body) {
window.removeChildren(this.ui.table.body);
@@ -1091,7 +1321,7 @@
} else {
document.removeEventListener('keydown', this.keyHandler);
}
- if (this.isTimeline && !on) this.toggleTimelineListeners(on);
+
}
renderGrid(items) {
@@ -1101,12 +1331,11 @@
this.ui.grid.classList.remove('list-view');
this.ui.grid.classList.add('grid-view');
- const fragment = document.createDocumentFragment();
- items.forEach(item => {
- let card = this.renderGridItem(item);
- fragment.appendChild(card);
- });
- this.ui.grid.appendChild(fragment);
+ window.chunkIt(
+ items,
+ (item) => this.renderGridItem(item),
+ (fragment) => this.ui.grid.append(fragment)
+ ).then(()=>{});
}
renderList(items) {
@@ -1115,221 +1344,56 @@
this.ui.grid.classList.remove('grid-view');
this.ui.grid.classList.add('list-view');
- const fragment = document.createDocumentFragment();
- items.forEach(item => {
- let row = this.renderListItem(item);
- fragment.append(row);
- });
- this.ui.grid.append(fragment);
+ window.chunkIt(
+ items,
+ (item) => this.renderListItem(item),
+ (fragment) => this.ui.grid.append(fragment)
+ ).then(()=>{});
}
- renderTable(items) {
+ async renderTable(items) {
this.toggleTable();
window.removeChildren(this.ui.grid);
- let fragment = document.createDocumentFragment();
- items.forEach(item => {
- let row = this.renderTableItem(item);
- if (row) fragment.append(row);
+ 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);
});
-
- if (this.ui.table.body) {
- this.ui.table.body.append(fragment);
- } else {
- this.ui.table.table.insertBefore(fragment, this.ui.table.foot);
- }
-
- if (this.isTimeline) {
- this.toggleTimelineListeners(true);
- }
}
/***************************************************************
RENDER HELPERS
***************************************************************/
- setupItemElement(element, item, templateName) {
- if (this.items[this.view].has(item.id)) {
- return this.items[this.view].get(item.id);
- }
-
- element.dataset.id = item.id;
- if (item._pending) element.classList.add('pending');
-
- // Setup selection checkbox/label
- this.setupItemSelection(element, item);
-
- // Setup thumbnail
- this.setupItemThumbnail(element, item);
-
- // Setup action buttons
- this.setupItemActions(element, item);
-
- this.items[this.view].set(item.id, element);
- return element;
- }
-
- setupItemSelection(element, item) {
- const checkbox = element.querySelector('.select-item');
- const label = element.querySelector('.select-item + label, .select-item-label');
- if (!checkbox) return;
-
- checkbox.id = `select-${item.id}`;
- checkbox.value = item.id;
- checkbox.checked = this.selected.has(parseInt(item.id));
-
- if (label) {
- label.htmlFor = `select-${item.id}`;
- }
- }
-
- setupItemThumbnail(element, item) {
- const img = element.querySelector('img');
- if (!img) return;
-
- const thumbnail = item.images?.[item.fields?.post_thumbnail] ?? {};
- img.src = thumbnail.medium ?? '';
- img.alt = thumbnail.alt ?? '';
- }
-
- setupItemActions(element, item) {
- element.querySelectorAll('[data-action]').forEach(btn => {
- btn.dataset.id = item.id;
- });
- }
-
renderGridItem(item) {
- if (this.items.grid.has(item.id)) {
- return this.items.grid.get(item.id);
- }
- const card = window.getTemplate('gridView');
- if (!card) return null;
- return this.setupItemElement(card, item, 'grid');
+ let gridItem = window.jvbTemplates.create('gridView', item);
+ this.items.set(item.id, gridItem);
+ return gridItem;
}
renderListItem(item) {
- if (this.items.list.has(item.id)) {
- return this.items.list.get(item.id);
- }
- let row = window.getTemplate('listView');
- if (!row) return null;
-
- row = this.setupItemElement(row, item, 'list');
-
- // List-specific: populate data attributes
- row.querySelectorAll('[data-attr]').forEach(el => {
- const value = item[el.dataset.attr];
- if (value && value !== '') {
- el.textContent = value;
- } else {
- el.remove();
- }
- });
-
- row.querySelectorAll('[data-field]').forEach(el => {
- const value = item.fields?.[el.dataset.field];
- if (value && value !== '') {
- el.tagName === 'DIV' ? el.innerHTML = value : el.textContent = value;
- } else {
- el.remove();
- }
- });
- this.items.list.set(item.id, row);
-
- return row;
+ let listItem = window.jvbTemplates.create('listView', item);
+ this.items.set(item.id, listItem);
+ return listItem;
}
renderTableItem(item) {
- if (this.items.table.has(item.id)) {
- return this.items.table.get(item.id);
- }
- let row = window.getTemplate('tableView');
- if (!row) return null;
-
- row = this.setupItemElement(row, item, 'table');
-
- const status = row.querySelector(`input[name="post_status"][value="${item.status}"]`);
- if (status) status.checked = true;
-
- if (this.isTimeline) {
- // Timeline: populate shared row, clone points
- const sharedRow = row.querySelector('tr.shared');
- if (sharedRow) {
- new window.jvbPopulate(sharedRow, item);
- this.cleanupTableRow(sharedRow);
- }
-
- const pointTemplate = row.querySelector('tr.timeline-point');
- if (pointTemplate && item.fields?.timeline) {
- Object.entries(item.fields.timeline).forEach(([imgId, timeline], index) => {
- const point = pointTemplate.cloneNode(true);
- point.dataset.index = index;
- point.dataset.imageId = imgId;
-
- new window.jvbPopulate(point, {
- fields: timeline,
- images: item.images,
- taxonomies: item.taxonomies
- });
- this.cleanupTableRow(point);
-
- const imgData = item.images?.[timeline.post_thumbnail];
- if (imgData) {
- point.querySelector('.field.upload')?.setAttribute('title', imgData['image-title'] || '');
- }
-
- row.insertBefore(point, pointTemplate);
- });
- pointTemplate.remove();
- }
- } else {
- // Standard table row
- if (this.ui.table.form?.dataset.edit !== undefined) {
- new window.jvbPopulate(row, item);
- } else {
- this.populateTableReadOnly(row, item);
- }
- this.cleanupTableRow(row);
- }
-
- this.items.table.set(item.id, row);
-
- return row;
+ let tableItem = window.jvbTemplates.create('tableView', item);
+ this.items.set(item.id, tableItem);
+ return tableItem;
}
- populateTableReadOnly(row, item) {
- for (const [key, value] of Object.entries(item)) {
- const col = row.querySelector(`[data-field="${key}"]`);
- if (!col) continue;
-
- const p = col.querySelector('p');
- if (p) {
- p.textContent = col.dataset.fieldType === 'date'
- ? window.formatTimeAgo(value)
- : value;
- }
- }
- }
-
- cleanupTableRow(row) {
- row.querySelectorAll('td[data-field]').forEach(field => {
- field.querySelectorAll('label:not(.select-item-label,.radio-option,[for*="select-item"])').forEach(label => {
- if (!label.closest('.radio-options')) {
- label.remove();
- }
- });
-
- //Remove toggle labels for true_false fields
- if (field.dataset.fieldType === 'true_false') {
- field.querySelector('.toggle-label')?.remove();
- }
-
- //Remove field label for checkbox/radio groups
- if (['checkbox','radio','select'].includes(field.dataset.fieldType)) {
- field.querySelector('.label')?.remove();
- }
-
- });
- }
toggleColumn(column, show) {
this.ui.table.table.querySelectorAll(`.${column}`).forEach(el =>{
el.hidden = !show;
@@ -1343,15 +1407,11 @@
|| newStatus !== this.store.filters.status;
}
removeItems(items) {
- let delay = 0;
items.forEach(itemId => {
- setTimeout(() => {
- let item = this.items[this.view].get(itemId);
- if (item) {
- window.fade(item, false);
- }
- }, delay);
- delay += 50;
+ if (this.items.has(itemId)) {
+ let item = this.items.get(itemId);
+ if (item) window.fade(item, false);
+ }
});
}
@@ -1371,9 +1431,13 @@
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);
- //TODO: If we set the element to checked, does that automatically call the change listener, which then also sets the store filter and cache?
this.store.setFilter(name, value);
}
@@ -1445,19 +1509,52 @@
/***************************************************************
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);
}
- for (let map of Object.values(this.items)) {
- map.clear();
- }
}
}
--
Gitblit v1.10.0