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/View.js | 504 ++++++++++++++++++++++++++++++++++++++++++-------------
1 files changed, 379 insertions(+), 125 deletions(-)
diff --git a/assets/js/concise/View.js b/assets/js/concise/View.js
index e1e5488..3f71831 100644
--- a/assets/js/concise/View.js
+++ b/assets/js/concise/View.js
@@ -12,13 +12,16 @@
this.store = store;
+ this.isTimeline = !!document.querySelector('[data-timeline]');
+
this.items = {
list: new Map(),
grid: new Map(),
table: new Map(),
}
- this.currentView = 'grid';
+ this.currentView = this.container.dataset.view ?? 'grid';
this.selectedItems = new Set();
+ this.subscribers = new Set();
this.init();
}
@@ -27,8 +30,11 @@
this.selectors = {
grid: '.item-grid',
table: {
- table: 'table',
+ table: 'form.table',
+ form: 'table',
body: 'table body',
+ header: 'table thead',
+ footer: 'table tfoot',
selectedColumns: '.all-filters .multi-select',
columns: 'thead th'
},
@@ -147,34 +153,30 @@
this.render();
});
});
- }
- /**
- * Handle data updates from store
- */
- handleDataUpdate(data) {
- console.log(data);
- const items = data.data?.items || data.items || [];
- this.render(items);
+ const checkedView = document.querySelector('[data-view]:checked');
+ if (checkedView) {
+ this.currentView = checkedView.dataset.view;
+ }
}
/**
* Handle items update
*/
handleItemsUpdate() {
- console.log(this.store.data);
- this.render(this.store.data);
+ this.render();
}
- render(items = []) {
+ render() {
if (!this.store) {
console.error('No store connected to renderer');
return;
}
- console.log(items);
+ const items = this.store.getFiltered();
// Handle empty state
if (items.length === 0) {
+ console.log('Nothing to show');
this.renderEmpty();
return;
}
@@ -216,24 +218,15 @@
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);
- }
- }
-
+ let card = this.renderGridItem(item);
fragment.appendChild(card);
});
this.ui.grid.appendChild(fragment);
}
renderGridItem(item) {
+ if (this.items.grid.has(item.id)) {
+ return this.items.grid.get(item.id);
+ }
const card = window.getTemplate('gridView');
card.dataset.id = item.id;
@@ -284,24 +277,45 @@
item.images[item.fields.post_thumbnail]?.medium??'',
item.images[item.fields.post_thumbnail]?.alt??'',
];
+
// }
+
+ this.items.grid.set(item.id, card);
return card;
}
toggleTable(on) {
- this.ui.table.selectedColumns.hidden = !on;
+ if (this.ui.table.selectedColumns) {
+ 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.form = this.ui.table.table.querySelector('table');
+ this.ui.table.header = this.ui.table.form.querySelector('thead');
+ this.ui.table.footer = this.ui.table.form.querySelector('tfoot');
+ this.ui.table.body = this.ui.table.form.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);
+ if (on) {
+ this.notify('table-view', this.ui.table.table);
+ }else {
+ this.notify('not-table-view', this.ui.table.table);
+ }
+
+ if (this.ui.table.body){
+ window.removeChildren(this.ui.table.body);
+ }
}
- this.ui.table.selectedColumns.hidden = !on;
+
+ if (this.ui.table.selectedColumns) {
+ this.ui.table.selectedColumns.hidden = !on;
+ }
}
toggleGrid() {
@@ -313,22 +327,29 @@
this.toggleGrid();
items.forEach(item => {
- let row;
- if (this.items.table.has(item.id)) {
- row = this.items.table.get(item.id);
+
+ let row = (this.isTimeline) ? this.renderTimelineTableItem(item) : this.renderTableItem(item);
+
+ if (this.ui.table.body) {
+ this.ui.table.body.append(row);
} else {
- row = this.store.renderOrRetrieve(item, 'table', this.renderTableItem.bind(this));
- this.items.table.set(item.id, row);
+ if (!this.ui.table.footer) {
+ this.ui.table.footer = this.ui.table.table.querySelector('tfoot');
+ }
+ this.ui.table.form.insertBefore(row, this.ui.table.footer);
}
- this.ui.table.body.append(row);
});
window.jvbSelector.scanExistingFields();
+
}
renderTableItem(item) {
- let empty = ['',0];
+ if (this.items.table.has(item.id)) {
+ return this.items.table.get(item.id);
+ }
+
const row = window.getTemplate('tableView');
row.dataset.id = item.id;
@@ -337,96 +358,171 @@
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
+ ];
+ let status = row.querySelector(`input[name="post_status"][value="${item.status}"]`);
+ if (status) {
+ status.checked = true;
+ }
+
+ if (Object.hasOwn(this.ui.table.table.dataset, 'edit')) {
+ new window.jvbPopulate(row, item);
+ } else {
+ for (let [key, value] of Object.entries(item)) {
+ let col = row.querySelector(`[data-field="${key}"]`);
+ if (col) {
+ let p = col.querySelector('p');
+ if (col.dataset.fieldType === 'date') {
+ value = window.formatTimeAgo(value);
+ }
+ p.textContent = value;
+ }
+ }
+ }
+
+
+ // Clean up after population
+ this.cleanupTableRow(row);
+
+ this.items.table.set(item.id, row);
+ return row;
+ }
+
+ renderTimelineTableItem(item) {
+ if (this.items.table.has(item.id)) {
+ return this.items.table.get(item.id);
+ }
+
+ 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,
+ ] = [
+ item.id,
+ item.id,
+ this.selectedItems.has(`${item.id}`),
+ item.id,
];
+ let timelinePoint = row.querySelector('.timeline-point');
+ let tbody = row;
- row.querySelectorAll('td[data-field]').forEach(field => {
- let value = item.fields[field.dataset.field];
- // field.querySelectorAll('label').forEach(label => {
- // label.hidden = true;
- // });
+ // Populate shared fields
+ let sharedRow = row.querySelector('tr.shared');
+ new window.jvbPopulate(sharedRow, item);
+ this.prefixTimelineFieldNames(sharedRow, item.id);
+ this.cleanupTableRow(sharedRow);
- 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;
+ // Handle timeline points
+ if (item.fields.timeline && typeof item.fields.timeline === 'object') {
+ const timelineArray = Object.entries(item.fields.timeline);
+
+ timelineArray.forEach(([imgId, timeline], index) => {
+ let point = timelinePoint.cloneNode(true);
+ point.dataset.index = index;
+ point.dataset.imageId = imgId;
+
+ // NEW: Create item-like structure for timeline point
+ const timelineItem = {
+ fields: timeline,
+ images: item.images,
+ taxonomies: {} // Timeline points don't have taxonomies
+ };
+
+ new window.jvbPopulate(point, timelineItem);
+
+ this.cleanupTableRow(point);
+ let imgdata = item.images[timeline.post_thumbnail];
+ if (imgdata) {
+ point.querySelector('.field.upload').title = imgdata['image-title'];
+ }
+
+ this.prefixTimelineFieldNames(point, timeline.id);
+
+ tbody.insertBefore(point, timelinePoint);
+ });
+ }
+
+ timelinePoint.remove();
+
+ this.items.table.set(item.id, row);
+ return row;
+ }
+
+ /**
+ * Timeline uses bracket notation: [postId]fieldName
+ * This matches the collectTimeline() method in FormController
+ */
+ prefixTimelineFieldNames(row, postId) {
+ row.querySelectorAll('input, textarea, select').forEach(field => {
+ const currentName = field.name;
+
+ if (!currentName || currentName.startsWith('[') ||
+ currentName === 'form-id' || currentName.startsWith('_')) {
+ return;
+ }
+
+ // Use bracket notation for timeline
+
+ let label = field.nextElementSibling;
+ field.name = `[${postId}]${currentName}`;
+ if (label && label.tagName === 'LABEL') {
+ field.id = `[${postId}]${field.id}`;
+ label.htmlFor = field.id;
}
});
- return row;
+ }
+
+ cleanupTableRow(row) {
+ row.querySelectorAll('td[data-field]').forEach(field => {
+ // Remove labels (they're in the header)
+ field.querySelectorAll('label:not(.select-item-label,.radio-option,[for*="select-item"])').forEach(label => {
+ if (!label.closest('.radio-options')) {
+ label.remove();
+ }
+ });
+
+ // Special handling for image/upload fields
+ // if (field.dataset.fieldType === 'image' || field.dataset.fieldType === 'upload') {
+ // const itemGrid = field.querySelector('.item-grid');
+ // const uploadContainer = field.querySelector('.file-upload-container');
+ //
+ // // If grid has items (populated), just remove upload UI
+ // if (itemGrid && itemGrid.children.length > 0) {
+ // // Remove upload controls but keep the populated items
+ // field.querySelectorAll('.progress, .upload-select, .status, details:not(.item-grid details)').forEach(el => {
+ // el.remove();
+ // });
+ // // Keep upload container hidden if it was hidden
+ // if (uploadContainer && uploadContainer.hidden) {
+ // uploadContainer.hidden = true;
+ // }
+ // } else {
+ // // No items, remove all upload UI
+ // field.querySelectorAll('.file-upload-wrapper, .progress, .upload-select, .status, details').forEach(el => {
+ // el.remove();
+ // });
+ // }
+ // }
+
+ // Remove toggle labels for true_false fields
+ if (field.dataset.fieldType === 'true_false') {
+ field.querySelector('.toggle-label')?.remove();
+ }
+
+ // Remove field labels for checkbox/radio groups
+ if (['checkbox', 'radio', 'select'].includes(field.dataset.fieldType)) {
+ field.querySelector('.label')?.remove();
+ }
+ });
}
renderList(items) {
@@ -437,19 +533,15 @@
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);
- }
-
+ let row = this.renderListItem(item);
this.ui.grid.appendChild(row);
});
}
renderListItem(item) {
+ if (this.items.list.has(item.id)) {
+ return this.items.list.get(item.id);
+ }
const row = window.getTemplate('listView');
row.dataset.id = item.id;
@@ -498,9 +590,158 @@
item.images[item.fields.post_thumbnail]?.alt??'',
]
}
+ this.items.list.set(item.id, row);
return row;
}
+ setupTimelineDragHandler() {
+ if (!this.isTimeline || this.currentView !== 'table') return;
+
+ // Clean up existing handler if any
+ if (this.timelineDragHandler) {
+ this.timelineDragHandler.destroy();
+ }
+
+ this.timelineDragHandler = new window.jvbDragHandler({
+ draggableSelector: '.timeline-point',
+ dropTargetSelector: '.timeline-point',
+ handleSelector: '.drag-handle',
+
+ getItemId: (element) => {
+ return element.dataset.imageId;
+ },
+
+ getSelectedItems: () => {
+ return [];
+ },
+
+ validateDrop: (itemIds, dropTarget) => {
+ const draggedRow = document.querySelector(`.timeline-point[data-image-id="${itemIds[0]}"]`);
+ if (!draggedRow) return false;
+
+ const draggedTbody = draggedRow.closest('tbody');
+ const targetTbody = dropTarget.closest('tbody');
+
+ return draggedTbody === targetTbody;
+ },
+
+ onDragStart: (itemIds, element) => {
+ element.classList.add('is-dragging');
+ },
+
+ onDrop: (itemIds, dropTarget) => {
+ const draggedRow = document.querySelector(`.timeline-point[data-image-id="${itemIds[0]}"]`);
+ if (!draggedRow) return;
+
+ // Remove all drop indicators
+ document.querySelectorAll('.drop-above, .drop-below').forEach(el => {
+ el.classList.remove('drop-above', 'drop-below');
+ });
+
+ const tbody = draggedRow.closest('tbody');
+ const dropPosition = dropTarget.dataset.dropPosition;
+
+ // Insert based on drop position
+ if (dropPosition === 'above') {
+ tbody.insertBefore(draggedRow, dropTarget);
+ } else {
+ tbody.insertBefore(draggedRow, dropTarget.nextSibling);
+ }
+
+ draggedRow.classList.remove('is-dragging');
+ this.updateTimelineOrder(tbody);
+ },
+
+ onDragEnd: (itemIds, success) => {
+ // Clean up all drag classes
+ document.querySelectorAll('.is-dragging, .drop-above, .drop-below').forEach(el => {
+ el.classList.remove('is-dragging', 'drop-above', 'drop-below');
+ });
+ },
+
+ previewElement: '.drag-handle',
+ previewOptions: {
+ offset: { x: -20, y: -20 },
+ showCount: false
+ }
+ });
+
+ // Add custom hover logic for better drop positioning
+ this.addTimelineDragHoverLogic();
+ }
+
+ addTimelineDragHoverLogic() {
+ let currentHover = null;
+
+ document.addEventListener('pointermove', (e) => {
+ if (!document.querySelector('.timeline-point.is-dragging')) return;
+
+ const target = e.target.closest('.timeline-point:not(.is-dragging)');
+ if (!target) {
+ if (currentHover) {
+ currentHover.classList.remove('drop-above', 'drop-below');
+ delete currentHover.dataset.dropPosition;
+ currentHover = null;
+ }
+ return;
+ }
+
+ // Determine if we're in the top or bottom half
+ const rect = target.getBoundingClientRect();
+ const midpoint = rect.top + (rect.height / 2);
+ const isTopHalf = e.clientY < midpoint;
+
+ // Update classes
+ if (currentHover && currentHover !== target) {
+ currentHover.classList.remove('drop-above', 'drop-below');
+ delete currentHover.dataset.dropPosition;
+ }
+
+ target.classList.remove('drop-above', 'drop-below');
+ target.classList.add(isTopHalf ? 'drop-above' : 'drop-below');
+ target.dataset.dropPosition = isTopHalf ? 'above' : 'below';
+
+ currentHover = target;
+ });
+ }
+ updateTimelineOrder(tbody) {
+ const postId = parseInt(tbody.dataset.id);
+ const rows = Array.from(tbody.querySelectorAll('.timeline-point'));
+
+ const item = this.store.get(postId);
+ if (!item) return;
+
+ let timeline = {};
+ // Update menu_order for each timeline point
+ rows.forEach((row, index) => {
+ const imgID = row.dataset.imageId;
+ timeline[imgID] = item.fields.timeline[imgID];
+ });
+ item.fields.timeline = timeline;
+
+ // Update store (triggers autosave)
+ this.store.save(item);
+ this.notify('order-changed', postId);
+ this.a11y?.announce(`Timeline order updated. ${rows.length} steps reordered.`);
+ }
+
+ extractRowFields(row) {
+ const fields = {};
+ row.querySelectorAll('[data-field]').forEach(cell => {
+ const fieldName = cell.dataset.field;
+ const input = cell.querySelector('input, textarea, select');
+
+ if (input) {
+ if (input.type === 'checkbox') {
+ fields[fieldName] = input.checked;
+ } else {
+ fields[fieldName] = input.value;
+ }
+ }
+ });
+ return fields;
+ }
+
toggleSelection(id) {
if (this.selectedItems.has(id)) {
this.selectedItems.delete(id);
@@ -545,6 +786,19 @@
this.ui.bulk.count.textContent = count === 0 ? '' : `${count} ${item} selected`;
}
}
+
+
+ /**
+ * Event system
+ */
+ subscribe(callback) {
+ this.subscribers.add(callback);
+ return () => this.subscribers.delete(callback);
+ }
+
+ notify(event, data) {
+ this.subscribers.forEach(cb => cb(event, data));
+ }
}
window.jvbViews = ViewController;
--
Gitblit v1.10.0