From d7dbe7fee362d587dfc334135d9581b6216a4295 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 23 Nov 2025 04:13:56 +0000
Subject: [PATCH] =Timeline block, and feed block updated. DataStore.js refactored to not block rendering
---
assets/js/concise/View.js | 460 +++++++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 348 insertions(+), 112 deletions(-)
diff --git a/assets/js/concise/View.js b/assets/js/concise/View.js
index e1e5488..ca6c99e 100644
--- a/assets/js/concise/View.js
+++ b/assets/js/concise/View.js
@@ -12,6 +12,8 @@
this.store = store;
+ this.isTimeline = !!document.querySelector('[data-timeline]');
+
this.items = {
list: new Map(),
grid: new Map(),
@@ -19,6 +21,7 @@
}
this.currentView = '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,6 +153,11 @@
this.render();
});
});
+
+ const checkedView = document.querySelector('[data-view]:checked');
+ if (checkedView) {
+ this.currentView = checkedView.dataset.view;
+ }
}
/**
@@ -162,16 +173,15 @@
* 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) {
@@ -216,24 +226,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,7 +285,10 @@
item.images[item.fields.post_thumbnail]?.medium??'',
item.images[item.fields.post_thumbnail]?.alt??'',
];
+
// }
+
+ this.items.grid.set(item.id, card);
return card;
}
@@ -294,12 +298,24 @@
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;
}
@@ -313,22 +329,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;
@@ -346,87 +369,142 @@
item.status
];
+ // Let jvbPopulate do its thing - NO prefixing needed!
+ new window.jvbPopulate(row, item.fields, item.images);
- row.querySelectorAll('td[data-field]').forEach(field => {
- let value = item.fields[field.dataset.field];
- // field.querySelectorAll('label').forEach(label => {
- // label.hidden = true;
- // });
+ // Clean up after population
+ this.cleanupTableRow(row);
- 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;
+ 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;
+
+ // Populate shared fields - NO prefixing!
+ let sharedRow = row.querySelector('tr.shared');
+ new window.jvbPopulate(sharedRow, item.fields, item.images);
+ this.prefixTimelineFieldNames(sharedRow, item.id);
+ this.cleanupTableRow(sharedRow);
+
+ // Handle timeline points - NO prefixing!
+ 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 window.jvbPopulate(point, timeline, item.images);
+
+ 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 +515,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 +572,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 +768,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