/** * Manages view states: grid, list, or table */ class ViewController { constructor(container, store) { this.a11y = window.jvbA11y; this.error = window.jvbError; this.container = container; this.initElements(); this.settings = window.jvbUserSettings; this.store = store; this.items = { list: new Map(), grid: new Map(), table: new Map(), } this.currentView = 'grid'; this.selectedItems = new Set(); this.init(); } initElements() { this.selectors = { grid: '.item-grid', table: { table: 'table', body: 'table body', selectedColumns: '.all-filters .multi-select', columns: 'thead th' }, bulk: { count: '.bulk-controls .selected-count', control: '.bulk-controls .bulk-actions', select: '.bulk-controls select', selectAll: '.select-all' } } this.ui = window.uiFromSelectors(this.selectors, this.container); } init() { // Subscribe to store updates this.store.subscribe((event, data) => { switch(event) { case 'items-saved': // this.handleDataUpdate(data); break; case 'data-loaded': this.handleItemsUpdate(); break; case 'item-saved': // this.updateItem(data.item); break; case 'item-deleted': // this.deleteItem(data.item); break; } }); // Set up view switcher this.setupViewSwitcher(); this.changeHandler = this.handleChange.bind(this); this.clickHandler = this.handleClick.bind(this); this.lastSelected = null; document.addEventListener('change', this.changeHandler); document.addEventListener('click', this.clickHandler); } handleClick(e) { let select = e.target.closest('.select-item-label'); if (select) { if (e.shiftKey) { e.preventDefault(); this.handleRangeSelection(e.target); } else { this.lastSelected = e.target.closest('.item'); } } } handleRangeSelection(target) { if (!this.lastSelected) { this.lastSelected = target.closest('.item'); return; } const current = target.closest('.item'); const all = Array.from(this.container.querySelectorAll('.item')); const lastIndex = all.indexOf(this.lastSelected); const currentIndex = all.indexOf(current); if (lastIndex === -1 || currentIndex === -1) { this.lastSelected = current; return; } const start = Math.min(lastIndex, currentIndex); const end = Math.max(lastIndex, currentIndex); let newSelections = 0; for (let i = start; i <= end; i++) { let item = all[i]; this.selectedItems.add(item.dataset.id); let checkbox = item.querySelector('.select-item'); if (checkbox && !checkbox.checked) { checkbox.checked = true; newSelections++; } } this.updateSelectionUI(); window.jvbA11y.announce(`Selected ${newSelections} items in range.`); } handleChange(e) { if (e.target.closest('.select-all')) { this.selectAll(e.target.checked); } else if (e.target.closest('.select-item')) { this.toggleSelection(e.target.closest('.item').dataset.id); } else if (e.target.closest('details.multi-select')) { this.toggleColumns(e.target.id, e.target.checked); } } toggleColumns(column, show) { let theColumn = this.ui.table.columns.filter(col => col.className === column); // console.log(theColumn); // console.log('Toggle Columns'); // console.log(column, show); // console.log(this.ui.table.columns); } setupViewSwitcher() { document.querySelectorAll('[data-view]').forEach(btn => { this.settings.addSetting(btn); btn.addEventListener('click', () => { this.currentView = btn.dataset.view; this.render(); }); }); } /** * Handle data updates from store */ handleDataUpdate(data) { console.log(data); const items = data.data?.items || data.items || []; this.render(items); } /** * Handle items update */ handleItemsUpdate() { console.log(this.store.data); this.render(this.store.data); } render(items = []) { if (!this.store) { console.error('No store connected to renderer'); return; } console.log(items); // Handle empty state if (items.length === 0) { this.renderEmpty(); return; } switch(this.currentView) { case 'grid': this.renderGrid(items); break; case 'table': this.renderTable(items); break; case 'list': this.renderList(items); break; } this.updateSelectionUI(); } renderEmpty() { this.toggleTable(false); window.removeChildren(this.ui.grid); const empty = window.getTemplate('emptyState'); if (empty) { this.ui.grid.appendChild(empty); this.a11y?.announce('No items found'); } } renderGrid(items) { this.toggleGrid(); 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; if (this.store.renderOrRetrieve) { card = this.store.renderOrRetrieve(item, 'grid', this.renderGridItem.bind(this)); } else { // Fallback to local cache if (this.items.grid.has(item.id)) { card = this.items.grid.get(item.id); } else { card = this.renderGridItem(item); this.items.grid.set(item.id, card); } } fragment.appendChild(card); }); this.ui.grid.appendChild(fragment); } renderGridItem(item) { const card = window.getTemplate('gridView'); card.dataset.id = item.id; if (item._pending) card.classList.add('pending'); let [ checkbox, label, img, edit, trash ] = [ card.querySelector('input'), card.querySelector('label'), card.querySelector('img'), card.querySelector('[data-action="edit"]'), card.querySelector('[data-action="trash"]'), ]; [ checkbox.value, checkbox.id, checkbox.checked, label.htmlFor, edit.dataset.id, trash.dataset.id ] = [ item.id, `select-${item.id}`, this.selectedItems.has(`${item.id}`), `select-${item.id}`, item.id, item.id ]; // if (this.store.config.storeName === 'progress') { // [ // img.src, // img.alt, // ] = [ // item.images[item.fields['timeline'][0].post_thumbnail]?.medium??'', // item.images[item.fields['timeline'][0].post_thumbnail]?.alt??'', // ]; // } else { [ img.src, img.alt, ] = [ item.images[item.fields.post_thumbnail]?.medium??'', item.images[item.fields.post_thumbnail]?.alt??'', ]; // } return card; } toggleTable(on) { this.ui.table.selectedColumns.hidden = !on; if (on && !this.ui.table.table) { let table = window.getTemplate('contentTable'); this.container.append(table); this.ui.table.table = this.container.querySelector('form.table'); this.ui.table.body = this.ui.table.table.querySelector('tbody'); this.ui.table.columns = this.container.querySelectorAll(this.selectors.table.columns); } if (this.ui.table.table) { this.ui.table.table.hidden = !on; window.removeChildren(this.ui.table.body); } this.ui.table.selectedColumns.hidden = !on; } toggleGrid() { window.removeChildren(this.ui.grid); } renderTable(items) { this.toggleTable(true); this.toggleGrid(); items.forEach(item => { let row; if (this.items.table.has(item.id)) { row = this.items.table.get(item.id); } else { row = this.store.renderOrRetrieve(item, 'table', this.renderTableItem.bind(this)); this.items.table.set(item.id, row); } this.ui.table.body.append(row); }); window.jvbSelector.scanExistingFields(); } renderTableItem(item) { let empty = ['',0]; const row = window.getTemplate('tableView'); row.dataset.id = item.id; [ row.querySelector('.select-item').id, row.querySelector('.select-item').value, row.querySelector('.select-item').checked, row.querySelector('.select-item + label').htmlFor, row.querySelector(`input[name="post_status"][value="${item.status}"]`).checked ] = [ item.id, item.id, this.selectedItems.has(`${item.id}`), item.id, item.status ]; row.querySelectorAll('td[data-field]').forEach(field => { let value = item.fields[field.dataset.field]; // field.querySelectorAll('label').forEach(label => { // label.hidden = true; // }); let label = field.querySelector('label'); let isEmpty = (empty.includes(value)); let temp; switch (field.dataset.fieldType) { case 'text': case 'number': case 'url': case 'tel': case 'email': if (!isEmpty) { field.querySelector('input').value = value; } label.remove(); break; case 'textarea': if (!isEmpty) { field.querySelector('textarea').value = value; } label.remove(); break; case 'taxonomy': label.remove(); if (!isEmpty) { temp = field.querySelector('input[type=hidden]'); temp.value = value; } break; case 'image': if (!isEmpty) { let image = window.getTemplate('uploadItem'); let img = image.querySelector('img'); [ img.src, img.alt ] = [ item.images[value].medium??'', item.images[value].alt??'', ]; field.querySelector('.item-grid').append(image); field.querySelector('input[type=hidden]').value = value; } field.querySelectorAll('.progress,label,.upload-select,.status,details').forEach(item => { item.remove(); }); break; case 'true_false': if (!isEmpty) { field.querySelector('input').checked = parseInt(value) === 1; } field.querySelector('.toggle-label')?.remove(); break; case 'select': label.remove(); case 'radio': case 'checkbox': field.querySelector('.label')?.remove(); if (!isEmpty) { value = value.split(','); value.forEach(v => { temp = field.querySelector(`[value="${v}"]`); if (temp) { temp.checked = true; } }); } break; default: if (!isEmpty) { console.log(value); } break; } }); return row; } renderList(items) { this.toggleGrid(); this.toggleTable(false); this.ui.grid.classList.remove('grid-view'); this.ui.grid.classList.add('list-view'); items.forEach(item => { let row; if (this.items.list.has(item.id)) { row = this.items.list.get(item.id); } else { row = this.store.renderOrRetrieve(item, 'list', this.renderListItem.bind(this)); this.items.list.set(item.id, row); } this.ui.grid.appendChild(row); }); } renderListItem(item) { const row = window.getTemplate('listView'); row.dataset.id = item.id; if (item._pending) row.classList.add('pending'); let select = row.querySelector('.select-item'); let label = row.querySelector('.select-item + label'); [ select.id, select.value, select.checked, label.htmlFor ] = [ item.id, item.id, this.selectedItems.has(`${item.id}`), item.id, ]; row.querySelectorAll('[data-attr]').forEach(attr => { if (item[attr.dataset['attr']] !== ''){ attr.textContent = item[attr.dataset['attr']]; } else { attr.remove(); } }); row.querySelectorAll('[data-field]').forEach(field => { let value = item.fields[field.dataset['field']]; if (value !== '') { if (field.tagName === 'DIV') { field.innerHTML = value; } else { field.textContent = value; } } else { field.remove(); } }); let img = row.querySelector('img'); if (img) { [ img.src, img.alt ] = [ item.images[item.fields.post_thumbnail]?.medium??'', item.images[item.fields.post_thumbnail]?.alt??'', ] } return row; } toggleSelection(id) { if (this.selectedItems.has(id)) { this.selectedItems.delete(id); } else { this.selectedItems.add(id); } this.updateSelectionUI(); } selectAll(check) { const items = this.container.querySelectorAll('.item'); if (!check) { this.selectedItems.clear(); this.ui.bulk.selectAll.checked = false; this.ui.bulk.select.value = ''; } items.forEach(item => { if (check) { this.selectedItems.add(item.dataset.id) } item.querySelector('.select-item').checked = check; }); this.updateSelectionUI(); } clearSelection() { this.selectAll(false); this.ui.bulk.select.value = ''; } updateSelectionUI() { const count = this.selectedItems.size; if (this.ui.bulk.control) { this.ui.bulk.control.hidden = count === 0; } if (this.ui.bulk.count) { let item = count === 1 ? 'item' : 'items'; this.ui.bulk.count.hidden = count === 0; this.ui.bulk.count.textContent = count === 0 ? '' : `${count} ${item} selected`; } } } window.jvbViews = ViewController;