From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh
---
assets/js/concise/CRUD.js | 2323 ++++++++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 1,745 insertions(+), 578 deletions(-)
diff --git a/assets/js/concise/CRUD.js b/assets/js/concise/CRUD.js
index 2c8694c..03ebd98 100644
--- a/assets/js/concise/CRUD.js
+++ b/assets/js/concise/CRUD.js
@@ -1,551 +1,1463 @@
-/**
- * Main CRUD Manager - Coordinates everything
- */
class CRUDManager {
- constructor(config) {
+ 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.config = config;
- this.content = config.content || false;
- this.settings = window.jvbUserSettings;
this.a11y = window.jvbA11y;
- if (!this.content) {
- return;
- }
+ this.error = window.jvbError;
+ this.populate = window.jvbPopulate;
+ this.cache = new window.jvbCache(this.content);
+
+ this.activeItem = null;
this.isTimeline = false;
- this.currentItemID = null;
+ this.isPopulating = false;
- this.initElements();
- this.updateBulkOptions();
-
- // Initialize components
- const store = window.jvbStore.register(
- this.content,
- {
- storeName: this.content,
- keyPath: 'id',
- endpoint: 'content',
- 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: {
- content: this.content,
- user: window.auth.getUser(),
- page: 1,
- status: 'all',
- orderby: 'modified', //or title
- order: 'desc'
- },
- TTL: 30 * 60 * 1000, //30 minutes cache
- showLoading: true,
- });
- this.store = store[this.content];
-
- this.status = 'all';
- this.filterTimeout = null;
-
- this.viewController = new window.jvbViews(this.ui.container, this.store);
- this.tableForm = null;
- this.tableChanges = new Map();
-
-
- this.formController = (this.isTimeline) ? new window.jvbForm({collectFormData: () => this.collectTimelineData.bind(this)}) : new window.jvbForm();
- this.viewController.subscribe((event, form) => {
- if (event === 'table-view' && !this.tableForm) {
- if (!this.tableForm) {
- this.tableForm = this.formController.registerForm(form, {
- autosave: false,
- formStatus: false,
- isTable: true,
- });
- }
-
- } else if (event === 'not-table-view') {
- if (this.tableForm) {
-
- }
- } else if (event === 'order-changed') {
- let data = this.store.get(form);
- if (!data) {
- return;
- }
- let changes = {};
- changes[form] = data;
- this.savePosts(changes, `Updating progression order`);
- }
- });
-
- this.formController.subscribe((event, data) => {
- switch(event) {
- case 'form-submit':
- case 'form-autosave':
- this.handleFormChange(event,data);
- break;
- }
- });
-
- this.queue.subscribe((event, data) => {
- if (!Object.hasOwn(data, 'endpoint') || !['content', 'uploads/groups'].includes(data.endpoint)) return;
- if (event === 'operation-completed') {
- this.handleQueueSuccess(event, data);
- } else if (event === 'operation-failed-permanent') {
- this.handleQueueFailure(event, data);
- }
- });
-
-
- // Track initialization
- this.initialized = false;
+ //Track Changes
+ this.changes = new Map();
+ this.items = new Map();
this.init();
}
- 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.currentItemID] = changes;
- this.savePosts(theChanges, title);
- return;
+ 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??'';
+ }
+
}
- let itemsToRemove = [];
- switch (true) {
- case data.config.element === this.ui.forms.edit:
- theChanges[this.currentItemID] = changes;
- title = `Saving ${title} Changes`;
- // Check if status change requires removal
- if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
- itemsToRemove.push(this.currentItemID);
- }
- break;
- case data.config.element === this.ui.forms.bulkEdit:
- let selected = data.config.element.querySelectorAll('.selected input:checked');
- selected.forEach(sel => {
- theChanges[sel.value] = changes;
- // Check if status change requires removal
- if (changes.post_status && this.shouldRemoveItem(changes.post_status)) {
- itemsToRemove.push(sel.value);
+ 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();
}
});
-
- title = `Updating ${selected.length} ${this.config.plural??'posts'} Changes`;
- break;
- case data.config.element === this.ui.forms.create:
- if (event === 'form-submit') {
- theChanges[data.config.data['form-id']] = changes;
- title = `Saving ${title} Changes`;
- }
- break;
- }
-
- // Handle visual removal with stagger effect
- if (itemsToRemove.length > 0) {
- let delay = 0;
- itemsToRemove.forEach(itemId => {
- setTimeout(() => {
- const element = document.querySelector(`.item[data-id="${itemId}"]`);
- if (element) {
- window.fade(element, false);
+ 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();
}
- }, delay);
- delay += 50; // Stagger by 50ms
- });
-
- // Clear selection after bulk edit with staggered removal
- if (data.config.element === this.ui.forms.bulkEdit) {
- setTimeout(() => {
- this.viewController.clearSelection();
- }, delay + 100);
+ });
}
+ });
+
+ let tableRefs = {};
+ let tableMany = {};
+ if (this.isTimeline) {
+ tableRefs.sharedRow = 'tr.shared';
+ tableRefs.point = 'tr.timeline-point';
}
-
- if (Object.keys(theChanges).length === 0) {
- return;
- }
-
- this.savePosts(theChanges, title);
- }
-
- shouldRemoveItem(newStatus) {
- return (this.status === 'all' && !['publish', 'draft'].includes(newStatus)) ||
- (newStatus !== this.status);
- }
-
- savePosts(changes, title) {
- if (Object.keys(changes).length === 0) {
- return;
- }
-
- //ensure content is in each post
- for (let postId in changes) {
- if (!changes[postId]['content']) {
- changes[postId]['content'] = this.content;
- }
- }
- let operation = {
- endpoint: 'content',
- headers: {
- 'action_nonce': window.auth.getNonce('dash'),
+ T.define('tableView', {
+ refs: {
+ checkbox: '.select-item',
+ selectLabel: 'label.select-item-label',
+ ... tableRefs,
},
- data: {
- posts: changes,
+ manyRefs: {
+ inputs: 'input,select,textarea',
+ status: 'input[name="post_status"]',
+ selectors: '[data-type="selector"]',
+ fields: '[data-field]',
+ ... tableMany,
},
- popup: `Saving changes`,
- title: title
- };
+ setup({ el, refs, manyRefs, data }) {
+ baseSetup(el, refs, data);
- this.queue.addToQueue(operation);
+ data.prefix = `${data.id}-`;
- }
- async handleQueueSuccess(event, data) {
- this.store.clearCache();
- this.store.fetch();
- }
- handleQueueFailure(event, data) {
- console.error('Operation failed permanently:', data);
- // Optionally show error notification to user
- this.a11y?.announce(`Operation failed: ${data.error_message || 'Unknown error'}`);
- }
+ window.jvbPopulate.populate(el, data);
- initElements() {
- this.elements = {
- modals: {
- create: 'dialog.create',
- edit: 'dialog.edit',
- bulkEdit: 'dialog.bulkEdit'
- },
- container: '.crud[data-content]',
- grid: '.item-grid',
- bulkSelectActions: '.bulk-action-select',
- forms: {
- create: 'dialog.create form',
- edit: 'dialog.edit form',
- bulkEdit: 'dialog.bulkEdit form'
- },
- uploader: 'details.uploader'
- };
- this.ui = window.uiFromSelectors(this.elements);
- if (this.ui.uploader) {
- window.jvbUploads.scanFields(document.querySelector(this.elements.uploader));
+ //
+ // 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);
+ });
- window.jvbUploads.subscribe((event, data) => {
- if (event === 'sent-to-queue') {
- console.log(data);
- if (data === this.ui.uploader.querySelector('[data-uploader]')?.dataset.uploader) {
- window.debouncer.schedule('crud-complete', ()=> {
- this.store.clearHttpHeaders();
+ 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',
+ }
+ },
+ wrap: '#item-wrap',
+ grid: `.${this.content}.item-grid`,
+ table: {
+ nav: '#vertical',
+ form: 'form.table',
+ table: 'form.table table',
+ body: 'form.table tbody',
+ head: 'form.table thead',
+ foot: 'form.table tfoot',
+ selectedColumns: '.all-filters .multi-select',
+ columns: 'thead th',
+ },
+ bulk: {
+ hasEdit: '.bulk-action-select [value="edit"]',
+ 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: 'details.uploader',
+ form: 'details.uploader form',
+ uploader: 'details.uploader [data-field-type="upload"]'
+ }
+ }
+
+ 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]');
}
- init() {
- if (this.ui.uploader){
- this.settings.addSetting(this.ui.uploader, 'open');
- this.ui.uploader.addEventListener('toggle', (e) =>{
- this.settings.saveSetting('open', this.ui.uploader.open ? 'on' : 'off');
+ initUploader() {
+ if (!this.ui.uploader.form) return;
+ this.uploadForm = this.forms.registerForm(this.ui.uploader.form).id??false;
+
+ // window.jvbUploads.scanFields(this.ui.uploader);
+ window.jvbUploads.subscribe((event, data) => {
+ if (event === 'sent-to-queue') {
+ if (data.field.id === this.ui.uploader.uploader.dataset.uploader) {
+ if (this.uploadForm ) {
+ this.forms.store.delete(this.uploadForm);
+ }
+
+ 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);
- // Set up filter controls
- this.filterHandler = this.handleFilterChange.bind(this);
- this.changeHandler = this.handleChange.bind(this);
+ 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.modals = {};
- for (let [name, modal] of Object.entries(this.ui.modals)) {
- this.modals[name] = new window.jvbModal(modal);
+ this.changesStore = stores['changes'];
+ this.store = stores[this.content];
- this.modals[name].subscribe((event, data) => {
- switch (event) {
- case 'modal-close':
- this.currentItemID = null;
- this.formController.cleanupForm(this.modals[name].modal.querySelector('form').dataset.formId);
- //double check we have finished saving
- break;
- case 'modal-open':
- //probably not needed in this class
- break;
+ 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: this.selectors.wrap,
+ },
+ item: {
+ idAttribute: 'itemId'
+ }
+ }, this.ui.views.table !== null);
+ 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);
}
});
}
- // Set up global event delegation
- this.setupEventDelegation();
+ this.queue.subscribe((event, data) => {
+ if (['image_upload', 'video_upload', 'document_upload'].includes(data.type)
+ && event === 'operation-status'
+ && data.status === 'completed') {
+ this.store.clearCache();
+ }
- this.setupFilters();
- this.initialized = true;
- }
+ 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);
+ }
- setupEventDelegation() {
- document.addEventListener('change', this.changeHandler);
- // Single event listener for all CRUD actions
- document.addEventListener('click', (e) => {
- // Check for action buttons
- const actionBtn = e.target.closest('[data-action]');
- if (actionBtn) {
- e.preventDefault();
- const action = actionBtn.dataset.action;
- const id = actionBtn.dataset.id;
+ this.store.clearCache();
+ }
- switch(action) {
- case 'edit':
- this.populateEditForm(id);
- this.modals.edit.handleOpen();
- break;
+ if (event === 'operation-status'
+ && data.status === 'completed'
+ && data.type === 'content_update') {
- case 'delete':
- if (confirm('Delete this item?')) {
- let changes = {};
- changes[actionBtn.dataset.id] = {
- 'post_status': 'delete',
- 'content': this.content
- };
- window.fade(actionBtn.closest('.item'), false);
- this.savePosts(changes, `Sending ${this.singular} to trash...`);
- this.store.delete(id);
- }
- break;
- case 'trash':
- let changes = {};
- changes[actionBtn.dataset.id] = {
- 'post_status': 'trash',
- 'content': this.content
- };
- window.fade(actionBtn.closest('.item'), false);
- this.savePosts(changes, `Sending ${this.singular} to trash...`);
- break;
+ this.store.clearCache();
- case 'create':
- this.modals.create.dataset.itemId = 'new';
- this.modals.create.dataset.content = this.content;
- this.modals.create.handleOpen();
- break;
+ if (!data.result || !data.result.success || !data.result.errors)
+ {
+ console.warn('Content update completed but no results', data);
+ return;
+ }
- case 'bulk-edit':
- const selected = Array.from(this.viewController.selectedItems);
- if (selected.length > 0) {
+ 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;
+ }
- this.modals.bulkEdit.handleOpen();
- }
- break;
+ if (Object.keys(data.result.success).length === 0) {
+ console.log(data.result.success);
+ data.result.success.forEach(id => this.changesStore.delete(id));
- case 'bulk-delete':
- const toDelete = Array.from(this.viewController.selectedItems);
- if (toDelete.length > 0 && confirm(`Delete ${toDelete.length} items?`)) {
- toDelete.forEach(id => this.store.delete(id));
- this.viewController.clearSelection();
- }
- break;
-
- case 'sync':
- // this.store.syncQueue();
- break;
-
- case 'refresh':
- this.store.fetch();
- break;
+ this.store.clearCache();
}
}
- let createButton = e.target.closest('.create-item');
- if (createButton) {
- this.formController.registerForm(this.ui.forms.create);
- this.modals.create.handleOpen();
- }
+ if (event === 'sent-to-server' && data.type === 'content_update') {
+ if (data instanceof FormData) return;
- let clearSelection = e.target.closest('.cancel-bulk');
- if (clearSelection) {
- this.viewController.selectAll(false);
- }
- });
-
- // Keyboard shortcuts
- document.addEventListener('keydown', (e) => {
- // Ctrl/Cmd + A to select all
- if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
- if (this.ui.container && this.ui.container.contains(document.activeElement)) {
- e.preventDefault();
- this.viewController.selectAll();
+ for ( let [id, changes] of Object.entries(data.posts)) {
+ this.compareStored(id, changes);
}
}
- // ESC to clear selection
- if (e.key === 'Escape' && this.viewController?.selectedItems.size > 0 && window.jvbModal.getAllModals().length === 0) {
- this.viewController.clearSelection();
- }
});
}
- handleChange(e) {
- if (e.target.closest('[data-id]')) {
- if (this.isTimeline) {
- this.handleTimelineTableChange(e);
+ 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.handleTableChange(e);
+ 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) {
+ this.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;
+ }
+ this.tabNav = tabDirection !== 'horizontal';
+
+
+
+ //Setup details open functionality
+ let details = {
+ showFilters: {
+ element: this.ui.filters.container,
+ default: 'closed',
+ },
+ showUploader: {
+ element: this.ui.uploader.details,
+ 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;
}
- if (e.target.classList.contains('bulk-action-select')) {
- if (e.target.value.startsWith('tax-')) {
- const taxonomy = e.target.value.replace('tax-', '');
- this.openTaxonomyModal(taxonomy);
- e.target.value = '';
+
+ 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 (this.view === 'table') {
+ this.handleTableChange(e);
+ }
+ 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;
+ }
+
+ }
+ handleTableChange(e) {
+ // 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 (e.target.value) {
+ switch(bulkAction.value) {
case 'edit':
- this.populateBulkEdit();
- this.modals.bulkEdit.handleOpen();
+ this.openBulkEditModal();
break;
case 'publish':
- this.setBulkStatus('publish');
+ case 'trash':
+ case 'delete':
+ this.setBulkStatus(bulkAction.value);
break;
case 'draft':
- this.setBulkStatus('draft');
- break;
- case 'trash':
- this.setBulkStatus('trash');
- break;
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);
+ }
+
+ 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':
- this.setBulkStatus('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;
}
}
- if (window.targetCheck(e, 'select[data-filter]')) {
- this.handleFilterChange(e);
- }
- }
- handleTableChange(e) {
- const row = e.target.closest('tr[data-id]');
- if (!row) return;
-
- const input = e.target;
- const postID = parseInt(row.dataset.id);
- const fieldName = input.closest(['data-field'])?.dataset.field;
- if (!fieldName) return;
-
- const item = this.store.get(postID);
- if (!item) return;
-
- item.fields[fieldName] = this.getInputValue(input);
-
- this.store.save(item);
-
- let post = {};
- post[postID] = item.fields;
- this.savePosts(post, `Saving changes to ${this.content}`);
- }
- handleTimelineTableChange(e) {
- const tbody = e.target.closest('tbody[data-id]');
- if (!tbody) return;
-
- const input = e.target;
- const fieldName = input.closest('[data-field]')?.dataset.field;
-
- if (!fieldName) return;
-
- const parentID = parseInt(tbody.dataset.id);
- const timelinePoint = input.closest('tr.timeline-point');
-
- const item = this.store.get(parentID);
- if (!item) return;
-
- const value = this.getInputValue(input);
-
- // Check if this is a specific point, or a shared value
- if (timelinePoint) {
- const imgID = timelinePoint.dataset.imageId;
- if (!item.fields.timeline) {
- item.fields.timeline = {};
+ 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();
}
- if (!item.fields.timeline[imgID]) {
- item.fields.timeline[imgID] = {};
- }
- item.fields.timeline[imgID][fieldName] = value;
- } else {
- item.fields[fieldName] = value;
}
- //Update store directly
- this.store.save(item);
+ handleInput(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ let query = e.target.value.trim();
+ let key = `${this.content}-search`;
- let changes = {};
- changes[parentID] = item.fields;
- this.savePosts(changes, 'Updating progress post');
- }
- 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;
- }
-
- openTaxonomyModal(taxonomy) {
- // Check if jvbSelector exists
- if (!window.jvbSelector) {
- console.error('TaxonomySelector not initialized');
+ if (query.length === 0) {
+ this.deleteFilter('search', '');
return;
}
- // Open the selector in filter mode
- window.jvbSelector.openForFilter(
- taxonomy,
- (selectedIds, taxonomy) => this.handleBulkTaxonomy(selectedIds, taxonomy)
+ // 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
);
}
- handleBulkTaxonomy(selectedIds, taxonomy) {
- // Callback when terms are selected
- if (selectedIds.length > 0) {
- selectedIds = selectedIds.join(',');
- let changes = {};
- let selected = Array.from(this.viewController.selectedItems);
+ handleKeys(e) {
+ if (!this.tabNav) return;
- selected.forEach(sel => {
- changes[sel] = {
- content: this.content
- };
- changes[sel][taxonomy] = selectedIds;
- });
+ if (e.key === 'Tab') {
+ e.preventDefault();
+ const currentCell = e.target.closest('[data-field]');
+ const currentRow = e.target.closest('tr');
- let title = `Adding ${selected.length} ${this.config.plural??'posts'} to ${selectedIds.length} ${jvbSettings.labels[taxonomy].plural}`;
- this.viewController.clearSelection();
- this.savePosts(changes, title);
+ 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.children)
+ .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`);
}
}
- setBulkStatus(status) {
- if (!['publish', 'draft', 'trash', 'delete'].includes(status)){
- return;
+ 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;
}
- let changes = {};
- for (let selected of this.viewController.selectedItems) {
- changes[selected] = {
- post_status: status,
- content: this.content
- };
+ 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.isPopulating = true;
+
+ this.modals.edit.handleOpen();
+ this.forms.registerForm(this.ui.modals.edit.form, {cache: false,
+ autoUpload: true,});
+
+
+ this.populate.populate(this.ui.modals.edit.form, item);
+ //For quill/taxonomy selector's async setups
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ setTimeout(() => {
+ this.isPopulating = false;
+ }, 50);
+ });
+ });
+
+ }
+ 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-WP-Nonce': window.auth.getNonce(),
+ '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':
@@ -554,155 +1466,410 @@
default:
title = window.uppercaseFirst(status)+'ing';
}
-
- if ((this.status === 'all' && !['publish', 'draft'].includes(status)) || status !== this.status) {
- let delay = 0;
- for (let selected of this.viewController.selectedItems) {
- setTimeout(() => {
- const element = document.querySelector(`.item[data-id="${selected}"]`);
- if (element) {
- window.fade(element, false);
- }
- }, delay);
- delay += 50; // Increment delay for staggered effect
- }
+ if (this.shouldRemoveItemUI(status)) {
+ this.removeItems(ids);
}
- // Clear selection even if items aren't being removed
- this.viewController.clearSelection();
+ this.selectionHandler.clearSelection();
+ this.savePosts(`${title} ${ids.length} ${ids.length === 1 ? this.singular : this.plural}...`).then(()=>{});
- if (Object.keys(changes).length !== 0) {
- this.savePosts(changes, `${title} ${this.viewController.selectedItems.size} ${this.plural}...`);
- }
}
-
- handleFilterChange(e) {
- let target = e.target;
- let filter = target.dataset.filter;
- if (filter === 'taxonomies') {
- let taxonomy = target.dataset.taxonomy;
- this.store.setFilter(`tax_${taxonomy}`, target.value);
- } else {
- this[target.dataset.filter] = target.value;
- this.store.setFilter(target.dataset.filter, target.value);
- if (target.dataset.filter === 'status') {
- this.updateBulkOptions(target.value);
- }
+ /***************************************************************
+ 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();
}
- updateBulkOptions(status = 'all') {
- if (status === 'trash') {
- if (this.ui.bulkSelectActions?.querySelector('[value="edit"]')) {
- window.removeChildren(this.ui.bulkSelectActions);
- let options = window.getTemplate('trashOptions');
- options.querySelectorAll('option').forEach((option, index) => {
- if (index === 0) {
- option.checked = true;
- }
- this.ui.bulkSelectActions.append(option);
+ 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.ui.wrap.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 {
- if (this.ui.bulkSelectActions && !this.ui.bulkSelectActions.querySelector('[value="edit"]')) {
- window.removeChildren(this.ui.bulkSelectActions);
+ document.removeEventListener('keydown', this.keyHandler);
+ }
- let options = window.getTemplate('notTrashOptions');
- options.querySelectorAll('option').forEach((option, index) => {
- this.ui.bulkSelectActions.append(option);
- });
- }
- }
- if (this.ui.bulkSelectActions) {
- this.ui.bulkSelectActions.value = '';
- }
}
- populateBulkEdit() {
- const container = this.modals.bulkEdit.modal.querySelector('form .selected');
- if (!container) return;
+ renderGrid(items) {
+ window.removeChildren(this.ui.grid);
+ this.toggleTable(false);
- window.removeChildren(container);
- for (let selected of this.viewController.selectedItems) {
- let item = this.store.get(selected);
+ this.ui.grid.classList.remove('list-view');
+ this.ui.grid.classList.add('grid-view');
- const img = window.getTemplate('bulkItem');
- if (!img) return;
-
- const checkbox = img.querySelector('input[type=checkbox]');
- const image = img.querySelector('img');
-
- if (checkbox) {
- checkbox.id = `bulk_${item.id}`;
- checkbox.value = item.id;
- checkbox.checked = true;
- }
-
- if (image && item.thumbnail) {
- image.src = item.thumbnail;
- image.alt = item.alt || '';
- }
-
- container.append(img);
- }
- let modal = this.modals.bulkEdit.modal;
- [
- modal.querySelector('h2 span').textContent
- ] = [
- this.viewController.selectedItems.size
- ];
-
- this.formController.registerForm(this.ui.forms.bulkEdit);
+ window.chunkIt(
+ items,
+ (item) => this.renderGridItem(item),
+ (fragment) => this.ui.grid.append(fragment)
+ ).then(()=>{});
}
- populateEditForm(itemID) {
- this.currentItemID = itemID;
+ renderList(items) {
+ window.removeChildren(this.ui.grid);
+ this.toggleTable(false);
- let item = this.store.get(parseInt(itemID));
- console.log('Item', item);
- if (item) {
- this.ui.modals.edit.dataset.itemId = itemID;
- this.ui.modals.edit.dataset.content = this.content;
-
- let form = this.ui.modals.edit.querySelector('form');
- [
- this.ui.modals.edit.querySelector('h2').textContent
- ] = [
- `Editing ${item.fields.post_title}`
- ];
- form.dataset.formId = `edit-${itemID}`;
- console.log('Sending to jvbPopulate: ', item.fields);
- console.log('and images: ', item.images);
- new window.jvbPopulate(form, item.fields, item.images);
- this.formController.registerForm(this.ui.forms.edit);
- }
+ 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(()=>{});
}
- setupFilters() {
- // Search
- const searchInput = document.querySelector('input[type="search"]');
- if (searchInput) {
- let searchTimeout;
- searchInput.addEventListener('input', () => {
- if (searchInput.value.length > 3) {
- clearTimeout(searchTimeout);
- searchTimeout = setTimeout(() => {
- this.store.setFilter('search', searchInput.value);
- }, 300);
- } else if (searchInput.value.length === 0) {
- this.store.removeFilter('search');
- }
- });
- }
- }
+ async renderTable(items) {
+ this.toggleTable();
+ window.removeChildren(this.ui.grid);
- destroy() {
- document.querySelectorAll('[data-filter]').forEach(filter => {
- filter.removeEventListener('change', this.filterHandler);
+
+ await window.chunkIt(
+ items,
+ (item) => this.renderTableItem(item),
+ (fragment) => {
+ this.ui.table.body.append(fragment);
+ },
+ 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) {
+ form.reset();
+
+ // 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);
+ }
+ }
}
-// Initialize when ready
document.addEventListener('DOMContentLoaded', async function() {
window.auth.subscribe((event) => {
if (event === 'auth-loaded') {
--
Gitblit v1.10.0