Jake Vanderwerf
2026-01-11 474109a5df0a06f5343ab184838fe2d80e3872a8
assets/js/concise/CRUD.js
@@ -1,29 +1,181 @@
/**
 * 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.cache = new window.jvbCache(this.content);
      this.activeItem = null;
      this.isTimeline = false;
      this.currentItemID = null;
      this.items = {
         list: new Map(),
         grid: new Map(),
         table: new Map()
      }; //DOM references
      this.init();
   }
   init() {
      this.initElements();
      this.updateBulkOptions();
      this.initListeners();
      let cached = this.initSettings();
      this.initStore(cached);
      this.checkHideFilters();
      this.initIntegrations();
      this.initUploader();
      this.initModals();
   }
      // Initialize components
   initElements() {
      this.allowedFilters = ['status', 'orderby', 'order', 'search', 'date-filter', 'dateFrom', 'dateTo'];
      this.selectors = {
         buttons: {
            create: '.create-item',
            clearFilters: '[data-action="clear-filters"]'
         },
         views: {
            grid: 'input[data-view="grid"]',
            list: 'input[data-view="list"]',
            table: 'input[data-view="table"]'
         },
         modals: {
            create: {
               modal: 'dialog.create',
               form: 'dialog.create form',
               h2: 'dialog.create h2',
            },
            edit: {
               modal: 'dialog.edit',
               form: 'dialog.edit form',
               h2: 'dialog.edit h2',
            },
            bulkEdit: {
               modal: 'dialog.bulkEdit',
               selected: 'dialog.bulkEdit .selected',
               h2: 'dialog.bulkEdit h2 span',
               form: 'dialog.bulkEdit form'
            },
            date: {
               modal: 'dialog.date-range',
               start: 'dialog.date-range .date-start',
               end: 'dialog.date-range .date-end',
               month: 'dialog.date-range .month-select',
            }
         },
         grid: `.${this.content}.item-grid`,
         table: {
            nav: '#vertical',
            form: 'form.table',
            table: 'form.table table',
            body: 'form.table body',
            head: 'form.table thead',
            foot: 'form.table tfoot',
            selectedColumns: '.all-filters .multi-select',
            columns: 'thead th',
         },
         bulk: {
            action: '.bulk-action-select',
            count: '.bulk-controls .selected-count',
            control: '.bulk-controls .bulk-actions',
            select: '.bulk-controls select',
            selectAll: '.select-all'
         },
         filters: {
            container: 'details.all-filters',
            search: '.all-filters input[type="search"]',
            status: {
               all: '[name="status"]#all',
               publish: '[name="status"]#publish',
               draft: '[name="status"]#draft',
               trash: '[name="status"]#trash',
            },
            orderby: {
               date: '[name="orderby"]#date',
               alphabetical: '[name="orderby"]#alphabetical',
            },
            order: {
               asc: '[name="order"][value="asc"]',
               desc: '[name="order"][value="desc"]'
            },
            date: '[data-filter="date"]'
         },
         uploader: 'details.uploader'
      }
      this.ui = window.uiFromSelectors(this.selectors);
      const taxFilters = document.querySelectorAll('[data-filter="taxonomies"]');
      if (taxFilters.length > 0) {
         this.ui.filters.taxonomies = {};
         taxFilters.forEach(tax => {
            const taxonomy = tax.dataset.taxonomy;
            this.ui.filters.taxonomies[taxonomy] = tax;
            this.allowedFilters.push(`tax_${taxonomy}`);
         });
      }
      this.isTimeline = !!document.querySelector('[data-timeline]');
   }
      initUploader() {
         if (!this.ui.uploader) return;
         window.jvbUploads.scanFields(this.ui.uploader);
         window.jvbUploads.subscribe((event, data) => {
            if (event === 'sent-to-queue') {
               if (data === this.ui.uploader.dataset.uploader) {
                  window.debouncer.schedule('crud-complete', ()=> {
                     this.store.clearCache();
                  });
               }
            }
         });
      }
      initModals() {
         this.modals = {};
         for (let [name, modal] of Object.entries(this.ui.modals)) {
            if (!modal.modal) continue;
            this.modals[name] = new window.jvbModal(modal.modal);
            this.modals[name].subscribe((event, data) => {
               switch (event) {
                  case 'modal-close':
                     const formId = this.ui.modals[name].form.dataset.formId;
                     if (formId) {
                        this.formController.cleanupForm(formId);
                     }
                     this.ui.modals[name].form.reset();
                     if (name === 'date') {
                        this.handleCustomDateSelection()
                     }
                     break;
                  case 'modal-open':
                     break;
               }
            })
         }
      }
   initStore(cached) {
      let filters = {
         ... this.defaults,
         ...cached
      };
      const store = window.jvbStore.register(
         this.content,
         {
            storeName: this.content,
            keyPath: 'id',
            endpoint: 'content',
            endpoint: this.endpoint??'content', //for taxonomy stores
            headers: {
               'action_nonce': window.auth.getNonce('dash'),
            },
@@ -34,52 +186,43 @@
               { 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
            filters: filters,
            ignore: ['content', 'user'],
            TTL: 60 * 60 * 1000,       //1 hour 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.store.subscribe((event, data) => {
         switch (event) {
            case 'data-loaded':
               this.render();
               this.selectionHandler.collectItems();
               break;
         }
      })
   }
   initIntegrations() {
      this.selected = new Set();
      this.selectionHandler = new window.jvbHandleSelection(this.container, {
         selectAll: {
            checkbox: '.crud #select-all',
            label: '.bulk-select label',
            span: '.bulk-select label span'
         },
         wrapper: {
            wrapper: '.wrap'
         }
      });
      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.formController = new window.jvbForm();
      this.formController.subscribe((event, data) => {
         switch(event) {
@@ -91,391 +234,740 @@
      });
      this.queue.subscribe((event, data) => {
         if (!Object.hasOwn(data, 'endpoint') || data.endpoint !== 'content') return;
         if (event === 'operation-completed') {
            this.handleQueueSuccess(event, data);
         } else if (event === 'operation-failed-permanent') {
            this.handleQueueFailure(event, data);
         if (['image_upload', 'video_upload', 'document_upload'].includes(data.type)
            && event === 'operation-status'
            && data.status === 'completed') {
            this.store.clearCache();
         }
      });
   }
   initSettings() {
      this.defaults = {
         content: this.content,
         user: window.auth.getUser(),
         page: 1,
         status: 'all',
         orderby: 'date',
         order: 'desc',
         search: '',
      }
      let updateFilters = {};
      //current view (defaults to grid)
      let defaultView = this.container.dataset.view??'grid'
      this.view = this.cache.get('view')??defaultView;
      if (this.view !== defaultView) {
         this.ui.views[this.view].checked = true;
      }
      //current status (defaults to all)
      this.status = this.cache.get('status')??this.defaults.status;
      if (this.status !== this.defaults.status) {
         this.ui.filters.status[this.status].checked = true;
         updateFilters.status = this.status;
      }
      //orderby & order
      this.orderby = this.cache.get('orderby')??this.defaults.orderby;
      if (this.orderby !== this.defaults.orderby) {
         this.ui.filters.orderby[this.orderby].checked = true;
         updateFilters.orderBy = this.orderby;
      }
      this.order = this.cache.get('order')??this.defaults.order;
      if (this.order !== this.defaults.order) {
         this.ui.filters.order[this.order].checked = true;
         updateFilters.order = this.order;
      }
      if (this.ui.filters.taxonomies) {
         Object.entries(this.ui.filters.taxonomies).forEach(([taxonomy, element]) => {
            const filterKey = `tax_${taxonomy}`;
            const cached = this.cache.get(filterKey);
            if (cached) {
               element.value = cached;
               updateFilters[filterKey] = cached;
            }
         });
      }
      let tabDirection = this.cache.get('tabNav')??'horizontal';
      if (this.ui.table.nav && tabDirection === 'vertical') {
         this.ui.table.nav.checked = true;
      }
      //Setup details open functionality
      let details = {
         showFilters: {
            element: this.ui.filters.container,
            default: 'closed',
         },
         showUploader: {
            element: this.ui.uploader,
            default: 'open'
         }
      };
      for (let [name, conf] of Object.entries(details)) {
         if (conf.element) {
            let cached = this.cache.get(name)??conf.default;
            conf.element.open = cached === 'open';
            conf.element.addEventListener('toggle', ()=> {
               this.cache.set(name, conf.element.open ? 'open' : 'closed');
            });
         }
      }
      return updateFilters;
   }
   /****************************************************************
    EVENT LISTENERS
   ****************************************************************/
   initListeners() {
      this.changeHandler = this.handleChange.bind(this);
      this.clickHandler = this.handleClick.bind(this);
      this.inputHandler = this.handleInput.bind(this);
      document.addEventListener('change', this.changeHandler);
      document.addEventListener('click', this.clickHandler);
      if (this.ui.filters.search) {
         this.ui.filters.search.addEventListener('input', this.inputHandler);
      }
   }
   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);
         return;
      }
      let filter = window.targetCheck(e, '[data-filter]');
      if (filter) {
         this.handleFilterChange(filter);
         return;
      }
      let view = window.targetCheck(e, 'input[data-view]');
      if (view) {
         this.handleViewChange(view);
         return;
      }
      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) {
            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');
         }
      }
   }
      handleBulkAction(bulkAction) {
         if (bulkAction.value.startsWith('tax-')) {
            const selectedOption = bulkAction.options[bulkAction.selectedIndex];
            const taxonomy = selectedOption.dataset.taxonomy;
            const single = selectedOption.dataset.single;
            const plural = selectedOption.dataset.plural;
            window.jvbSelector.openEmpty(
               taxonomy,
               single,
               plural,
               (result) => this.handleBulkTaxonomy(result)
            );
            bulkAction.value = '';
            return;
         }
         switch(bulkAction.value) {
            case 'edit':
               this.openBulkEditModal();
               break;
            case 'publish':
            case 'trash':
            case 'delete':
               this.setBulkStatus(bulkAction.value);
               break;
            case 'draft':
            case 'restore':
               this.setBulkStatus('draft');
               break;
         }
      }
   handleBulkTaxonomy(result) {
      if (!result.termIds.length || !this.selected.size) return;
      const changes = {};
      const taxonomyField = `tax_${result.taxonomy}`;
      this.selected.forEach(itemID => {
         const item = this.store.get(parseInt(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])];
         changes[itemID] = {
            [taxonomyField]: newIds.join(','),
            content: this.content
         };
      });
      if (Object.keys(changes).length > 0) {
         this.savePosts(
            changes,
            `Adding ${result.terms.length} ${result.taxonomy} to ${this.selected.size} ${this.plural}...`
         );
      }
      this.selectionHandler.clearSelection();
   }
   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) {
      let clearSearch = window.targetCheck(e, '.clear-search');
      if (clearSearch) {
         this.deleteFilter('search', '');
      }
      let actionButton = window.targetCheck(e, '[data-action]');
      if (actionButton) {
         e.preventDefault();
         let itemID = actionButton.dataset.id;
         switch (actionButton.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.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...`);
               break;
            case 'bulk-edit':
               if (this.selected.size > 0) {
                  this.openBulkEditModal();
               }
               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();
               }
               break;
            case 'refresh':
               this.store.clearCache();
               this.store.fetch();
               break;
            case 'clear-filters':
               this.clearAllFilters();
               break;
         }
      }
      const applyDate = window.targetCheck(e, '.apply-date-filter');
      if (applyDate) {
         this.handleCustomDateSelection();
         this.modals.date.handleClose();
         return;
      }
      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();
      let query = e.target.value.trim();
      let key = `${this.content}-search`;
      if (query.length === 0) {
         this.deleteFilter('search', '');
         return;
      }
      // Require minimum 2 characters
      // if (query.length < 2) {
      //    return;
      // }
      window.debouncer.schedule(
         key,
         () => {
            this.a11y.announce(`Searching for "${query}"...`);
            this.store.setFilters({ search: query, page: 1 });
         },
         300
      );
   }
   handleKeys(e) {
      if (!this.tabNav) return;
      if (e.key === 'Tab') {
         e.preventDefault();
         const currentCell = e.target.closest('[data-field]');
         const currentRow = e.target.closest('tr');
         if (!currentCell || !currentRow) return;
         const fieldName = currentCell.dataset.field;
         const isShift = e.shiftKey;
         // Find next editable row
         let targetRow = this.findNextEditableRow(currentRow, isShift);
         // If no target row found, wrap around
         if (!targetRow) {
            targetRow = this.wrapToRow(currentRow, isShift);
         }
         if (targetRow) {
            this.focusFieldInRow(targetRow, fieldName, isShift);
         }
      }
   }
   findNextEditableRow(currentRow, goBackward = false) {
      let row = goBackward ? currentRow.previousElementSibling : currentRow.nextElementSibling;
      // For timeline tables, skip non-editable rows
      while (row && !this.isEditableRow(row)) {
         row = goBackward ? row.previousElementSibling : row.nextElementSibling;
      }
      return row;
   }
   wrapToRow(currentRow, goBackward = false) {
      if (this.isTimeline) {
         // For timeline, stay within the same tbody
         const tbody = currentRow.closest('tbody');
         if (!tbody) return null;
         const rows = Array.from(tbody.querySelectorAll('tr'))
            .filter(row => this.isEditableRow(row));
         return goBackward ? rows[rows.length - 1] : rows[0];
      } else {
         // For regular tables, use all rows in tbody
         if (!this.ui.table.body) return null;
         const rows = Array.from(this.ui.table.body.querySelectorAll('tr'))
            .filter(row => this.isEditableRow(row));
         return goBackward ? rows[rows.length - 1] : rows[0];
      }
   }
   isEditableRow(row) {
      // Skip thead/tfoot
      if (row.closest('thead') || row.closest('tfoot')) {
         return false;
      }
      // For timeline, check for specific classes
      if (this.isTimeline) {
         return row.classList.contains('shared') || row.classList.contains('timeline-point');
      }
      // For regular tables, check for data-id
      return !!row.dataset.id;
   }
   focusFieldInRow(row, fieldName, fromAbove = false) {
      const targetCell = row.querySelector(`[data-field="${fieldName}"]`);
      if (!targetCell) return;
      const input = this.findFocusableInput(targetCell);
      if (input) {
         input.focus();
         // Select text if it's a text input
         if (input.select && input.type === 'text') {
            input.select();
         }
         // Announce for accessibility
         const direction = fromAbove ? 'next' : 'previous';
         this.a11y?.announce(`Moved to ${fieldName} in ${direction} row`);
      }
   }
   findFocusableInput(cell) {
      const selectors = [
         'input:not([type="hidden"]):not([disabled])',
         'textarea:not([disabled])',
         'select:not([disabled])',
         'button:not([disabled])'
      ];
      for (const selector of selectors) {
         const element = cell.querySelector(selector);
         if (element) return element;
      }
      return null;
   }
   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;
      // Track initialization
      this.initialized = false;
      // Save to store and server
      this.store.save(item);
      this.savePosts(
         { [itemID]: { timeline: reorderedTimeline, content: this.content } },
         'Reordering timeline...',
         true
      );
      this.init();
      this.a11y?.announce(`Moved to position ${newIndex + 1}`);
   }
   /*******************************************************************
    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;
      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.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??''
         }
         label.title = item.fields['post_title']??'';
         this.ui.modals.bulkEdit.selected.append(template);
      });
      if (this.ui.modals.bulkEdit.h2) {
         this.ui.modals.bulkEdit.h2.textContent = this.selected.size;
      }
      this.formController.registerForm(this.ui.modals.bulkEdit.form);
   }
   /*****************************************************************
    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.currentItemID] = changes;
         this.savePosts(theChanges, title);
         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;
      }
      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);
      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);
         }
      } 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);
            }
            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);
               }
            });
            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);
               }
            }, delay);
            delay += 50; // Stagger by 50ms
            num++;
         });
         // Clear selection after bulk edit with staggered removal
         if (data.config.element === this.ui.forms.bulkEdit) {
            setTimeout(() => {
               this.viewController.clearSelection();
            }, delay + 100);
         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);
      this.savePosts(theChanges, title, event === 'form-submit');
   }
   savePosts(changes, title, delay = false) {
      if (Object.keys(changes).length === 0) return;
   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 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: 'content',
         endpoint: this.endpoint,
         headers: {
            'action_nonce': window.auth.getNonce('dash'),
         },
         data: {
            posts: changes,
            posts: validChanges,
         },
         delay: delay,
         popup: `Saving changes`,
         title: title
      };
      this.queue.addToQueue(operation);
   }
   async handleQueueSuccess(event, data) {
      this.store.clearCache();
      this.store.clearHttpHeaders();
      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'}`);
   }
   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);
      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');
         });
      }
      // Set up filter controls
      this.filterHandler = this.handleFilterChange.bind(this);
      this.changeHandler = this.handleChange.bind(this);
      this.modals = {};
      for (let [name, modal] of Object.entries(this.ui.modals)) {
         this.modals[name] = new window.jvbModal(modal);
         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;
            }
         });
      }
      // Set up global event delegation
      this.setupEventDelegation();
      this.setupFilters();
      this.initialized = true;
   }
   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;
            switch(action) {
               case 'edit':
                  this.populateEditForm(id);
                  this.modals.edit.handleOpen();
                  break;
               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;
               case 'create':
                  this.modals.create.dataset.itemID = 'new';
                  this.modals.create.dataset.content = this.content;
                  this.modals.create.handleOpen();
                  break;
               case 'bulk-edit':
                  const selected = Array.from(this.viewController.selectedItems);
                  if (selected.length > 0) {
                     this.modals.bulkEdit.handleOpen();
                  }
                  break;
               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;
            }
         }
         let createButton = e.target.closest('.create-item');
         if (createButton) {
            this.formController.registerForm(this.ui.forms.create);
            this.modals.create.handleOpen();
         }
         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();
            }
         }
         // 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);
         } else {
            this.handleTableChange(e);
         }
         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 = '';
            return;
         }
         switch (e.target.value) {
            case 'edit':
               this.populateBulkEdit();
               this.modals.bulkEdit.handleOpen();
               break;
            case 'publish':
               this.setBulkStatus('publish');
               break;
            case 'draft':
               this.setBulkStatus('draft');
               break;
            case 'trash':
               this.setBulkStatus('trash');
               break;
            case 'restore':
               this.setBulkStatus('draft');
               break;
            case 'delete':
               this.setBulkStatus('delete');
               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 container = this.isTimeline
         ? e.target.closest('tbody[data-id]')
         : e.target.closest('tr[data-id]');
      if (!container) return;
      const input = e.target;
      const postID = parseInt(row.dataset.id);
      const fieldName = input.closest(['data-field'])?.dataset.field;
      if (!fieldName) return;
      const field = input.closest('[data-field]')?.dataset.field;
      if (!field) 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);
      const itemID = parseInt(container.dataset.id);
      const item = this.store.get(itemID);
      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 = {};
      // 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;
         }
         if (!item.fields.timeline[imgID]) {
            item.fields.timeline[imgID] = {};
         }
         item.fields.timeline[imgID][fieldName] = value;
      } else {
         item.fields[fieldName] = value;
         item.fields[field] = value;
      }
      //Update store directly
      this.store.save(item);
      let changes = {};
      changes[parentID] = item.fields;
      this.savePosts(changes, 'Updating progress post');
      this.savePosts({ [itemID]: item.fields }, `Saving changes...`, true);
   }
   getInputValue(input) {
      if (input.type === 'checkbox') {
         return input.checked ? (input.value || '1') : '';
@@ -485,54 +977,16 @@
      }
      return input.value;
   }
   openTaxonomyModal(taxonomy) {
      // Check if jvbSelector exists
      if (!window.jvbSelector) {
         console.error('TaxonomySelector not initialized');
         return;
      }
      // Open the selector in filter mode
      window.jvbSelector.openForFilter(
         taxonomy,
         (selectedIds, taxonomy) => this.handleBulkTaxonomy(selectedIds, taxonomy)
      );
   }
   handleBulkTaxonomy(selectedIds, taxonomy) {
      // Callback when terms are selected
      if (selectedIds.length > 0) {
         selectedIds = selectedIds.join(',');
         let changes = {};
         let selected = Array.from(this.viewController.selectedItems);
         selected.forEach(sel => {
            changes[sel] = {
               content: this.content
            };
            changes[sel][taxonomy] = selectedIds;
         });
         let title = `Adding ${selected.length} ${this.config.plural??'posts'} to ${selectedIds.length} ${jvbSettings.labels[taxonomy].plural}`;
         this.viewController.clearSelection();
         this.savePosts(changes, title);
      }
   }
   setBulkStatus(status) {
      if (!['publish', 'draft', 'trash', 'delete'].includes(status)){
         return;
      }
      if (!['publish', 'draft', 'trash', 'delete'].includes(status)) return;
      let changes = {};
      for (let selected of this.viewController.selectedItems) {
         changes[selected] = {
      this.selected.forEach(itemID => {
         changes[itemID] = {
            post_status: status,
            content: this.content
         };
      }
      });
      let title;
      switch (status) {
         case 'delete':
@@ -541,152 +995,472 @@
         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(Object.keys(changes));
      }
      // Clear selection even if items aren't being removed
      this.viewController.clearSelection();
      this.selectionHandler.clearSelection();
      if (Object.keys(changes).length !== 0) {
         this.savePosts(changes, `${title} ${this.viewController.selectedItems.size} ${this.plural}...`);
         this.savePosts(changes, `${title} ${Object.keys(changes).length} ${this.plural}...`);
      }
   }
   /***************************************************************
    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);
            break;
         case 'list':
            this.renderList(items);
            break;
      }
      this.updateUI();
   }
   updateUI() {
      if (this.ui.bulk.action) {
         let options = false;
         let hasEdit =this.ui.bulk.action.querySelector('[value="edit"]');
         if (this.status === 'trash' && hasEdit) {
            window.removeChildren(this.ui.bulk.action);
            options = window.getTemplate('trashOptions');
         } else if (this.status !== 'trash' && !hasEdit) {
            window.removeChildren(this.ui.bulk.action);
            options = window.getTemplate('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();
      }
   }
   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);
         }
   renderEmpty() {
      this.toggleTable(false);
      window.removeChildren(this.ui.grid);
      const empty = window.getTemplate('emptyState');
      if (empty) {
         this.ui.grid.append(empty);
         this.a11y.announceItems(0,false,false);
      }
   }
   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;
   toggleTable(on = true) {
      if (this.ui.table.selectedColumns) this.ui.table.selectedColumns.hidden = !on;
      if (on && !this.ui.table.form) {
         let table = window.getTemplate('contentTable');
         this.container.append(table);
         this.ui.table = window.uiFromSelectors(this.selectors.table);
         this.ui.table.columns = this.container.querySelectorAll(this.selectors.table.columns);
      }
      if (this.ui.table.form) {
         this.ui.table.form.hidden = !on;
         if (on) {
            this.formController.registerForm(this.ui.table.form, {
               autosave: false,
               formStatus: false,
               isTable: true
            });
         } else {
            this.formController.cleanupForm(this.ui.table.form.dataset.formId)
         }
         if (this.ui.table.body) {
            window.removeChildren(this.ui.table.body);
         }
      }
      this.keyHandler = this.handleKeys.bind(this);
      if (on) {
         document.addEventListener('keydown', this.keyHandler);
      } else {
         document.removeEventListener('keydown', this.keyHandler);
      }
      if (this.isTimeline && !on) this.toggleTimelineListeners(on);
   }
   renderGrid(items) {
      window.removeChildren(this.ui.grid);
      this.toggleTable(false);
      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);
   }
   renderList(items) {
      window.removeChildren(this.ui.grid);
      this.toggleTable(false);
      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);
   }
   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);
      });
      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');
   }
   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;
   }
   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'] || '');
               }
               this.ui.bulkSelectActions.append(option);
               row.insertBefore(point, pointTemplate);
            });
            pointTemplate.remove();
         }
      } else {
         if (this.ui.bulkSelectActions && !this.ui.bulkSelectActions.querySelector('[value="edit"]')) {
            window.removeChildren(this.ui.bulkSelectActions);
            let options = window.getTemplate('notTrashOptions');
            options.querySelectorAll('option').forEach((option, index) => {
               this.ui.bulkSelectActions.append(option);
            });
         // Standard table row
         if (this.ui.table.form?.dataset.edit !== undefined) {
            new window.jvbPopulate(row, item);
         } else {
            this.populateTableReadOnly(row, item);
         }
         this.cleanupTableRow(row);
      }
      if (this.ui.bulkSelectActions) {
         this.ui.bulkSelectActions.value = '';
      this.items.table.set(item.id, row);
      return row;
   }
   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;
         }
      }
   }
   populateBulkEdit() {
      const container = this.modals.bulkEdit.modal.querySelector('form .selected');
      if (!container) return;
      window.removeChildren(container);
      for (let selected of this.viewController.selectedItems) {
         let item = this.store.get(selected);
         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);
   }
   populateEditForm(itemID) {
      this.currentItemID = itemID;
      let item = this.store.get(parseInt(itemID));
      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}`;
         new window.jvbPopulate(form, item.fields, item.images);
         this.formController.registerForm(this.ui.forms.edit);
      }
   }
   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');
   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;
      });
   }
   /***************************************************************
    UTILITY
   ***************************************************************/
   shouldRemoveItemUI(newStatus) {
      return (this.status === 'all' && !['publish', 'draft'].includes(newStatus))
         || 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;
      });
   }
   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);
      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);
   }
   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
   ***************************************************************/
   destroy() {
      document.querySelectorAll('[data-filter]').forEach(filter => {
         filter.removeEventListener('change', this.filterHandler);
      });
      if (this.timelineSortables) {
         this.timelineSortables.forEach(sortable => sortable.destroy());
         this.timelineSortables = [];
      }
      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();
      }
   }
}
// Initialize when ready
document.addEventListener('DOMContentLoaded', async function()  {
   window.auth.subscribe((event) => {
      if (event === 'auth-loaded') {