From 235ce5716edc2f7cbe80fdccf26eac7269587839 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 08 Jun 2026 04:38:18 +0000
Subject: [PATCH] =FavouritesManager.php and FavouritesRoutes.php fixes. Moving all logic to FavouritesManager.php. Still some left to do
---
assets/js/concise/HandleSelection.js | 336 +++++++++++++++++++++++++++++++++----------------------
1 files changed, 202 insertions(+), 134 deletions(-)
diff --git a/assets/js/concise/HandleSelection.js b/assets/js/concise/HandleSelection.js
index 3034a1c..4d38f4e 100644
--- a/assets/js/concise/HandleSelection.js
+++ b/assets/js/concise/HandleSelection.js
@@ -1,25 +1,77 @@
class HandleSelection {
- constructor(options) {
- this.container = options.container; // An actual element, not class name
- this.selectors = {
- item: options.item || '.item',
- count: options.count || '.selection-count',
- bulkControls: options.bulkControls || '.selection-actions',
- checkbox: options.checkbox || '[name*="select-item"]',
- selectAll: options.selectAll || '[data-select-all]',
- wrapper: options.wrapper || ':has(.item-grid)'
+ constructor(container, options = {}) {
+ this.container = container;
+ const defaults = {
+ selectAll: {
+ checkbox: '[data-select-all]',
+ label: '.selected label',
+ span: '.selected label span',
+ target: 'data-selects',
+ count: '.selected-count, .selected .info',
+ bulkControls: '.bulk-actions',
+ },
+ items: '.item-grid',
+ wrapper: {
+ wrapper: ':has(.item-grid, [data-select-all])',
+ id: 'selection',
+ },
+ item: {
+ item: '.item',
+ idAttribute: 'id',
+ checkbox: '[name="select-item"]',
+ },
+ wrappers: {},
};
+ this.selectors = window.deepMerge(defaults, options);
- this.ui = window.uiFromSelectors(this.selectors, this.container)
+ this.a11y = window.jvbA11y;
this.selectedItems = new Set();
this.lastSelected = null; // For shift+click range selection
this.lastSelectedWrapper = null; //Tracks which wrapper we're in
this.lastClicked = null;
this.subscribers = new Set();
+ this.items = new Map();
+ this.initElements();
this.initListeners();
+ //store items in memory if available
+ this.collectItems();
}
+ removeDataReferences() {
+ let selectors = JSON.parse(JSON.stringify(this.selectors));
+ delete selectors.item.idAttribute;
+ delete selectors.wrapper.id;
+ delete selectors.selectAll.target;
+ return selectors;
+ }
+ initElements() {
+ this.index = 0;
+ let selectors = this.removeDataReferences();
+ this.ui = window.uiFromSelectors(selectors, this.container);
+
+ this.container.querySelectorAll(this.selectors.wrapper.wrapper).forEach(wrapper => {
+ this.addWrapper(wrapper);
+ });
+ }
+ addWrapper(el) {
+ let id = this.selectors.wrapper.id;
+ if (!Object.hasOwn(el.dataset, id)) {
+ el.setAttribute(`data-${id}`, this.index);
+ this.index++;
+ }
+ let selectors = this.removeDataReferences().selectAll;
+
+ //store the DOM of the grid and selectAll
+ this.ui.wrappers[el.dataset[id]] = {
+ element: el,
+ items: el.querySelector(this.selectors.items),
+ selectAll: window.uiFromSelectors(selectors, el)
+ };
+ }
+ removeWrapper(el) {
+ delete this.ui.wrappers[el.dataset[this.selectors.wrapper.id]];
+ }
initListeners() {
this.clickHandler = this.handleClick.bind(this);
@@ -28,29 +80,29 @@
this.container.addEventListener('change', this.changeHandler);
this.container.addEventListener('click', this.clickHandler);
- this.container.addEventListener('keydown', this.keyHandler);
+ document.addEventListener('keydown', this.keyHandler);
}
handleChange(e) {
- // Select all
- if (e.target.matches(this.selectors.selectAll)) {
+ if (!this.container.contains(e.target)) return;
+
+ // Select all checkbox
+ if (e.target.matches(this.selectors.selectAll.checkbox)) {
this.handleSelectAll(e.target);
return;
}
- // Individual checkbox
- if (e.target.matches(this.selectors.checkbox)) {
- const item = e.target.closest(this.selectors.item);
+ // Individual checkbox - only process if not already handled by shift-click
+ if (e.target.matches(this.selectors.item.checkbox)) {
+ const item = e.target.closest(this.selectors.item.item);
if (!item) return;
- // Find the immediate wrapper - check group first, then preview
- const wrapper = this.getItemWrapper(item);
- const id = this.getItemId(item);
+ const wrapper = this.getItemWrapper(e.target);
+ if (!wrapper) return;
- // Clear selection if clicking in different wrapper without shift
- if (this.lastSelectedWrapper && wrapper && wrapper !== this.lastSelectedWrapper && !e.shiftKey) {
- this.clearSelection();
- }
+ this.lastClicked = wrapper.element;
+ const id = this.getItemId(item);
+ if (!id) return;
if (e.target.checked) {
this.select(id, false);
@@ -59,42 +111,38 @@
}
this.lastSelected = id;
- this.lastSelectedWrapper = wrapper;
+ this.lastSelectedWrapper = wrapper.element;
+ this.updateSelectionUI();
}
}
handleClick(e) {
- const item = e.target.closest(this.selectors.item);
- const wrapper = item ? this.getItemWrapper(item) : null;
-
- if (wrapper) {
- this.lastClicked = wrapper;
- }
-
- // Handle non-checkbox clicks on items
- if (item && !e.target.matches(this.selectors.checkbox)) {
- if (this.lastSelectedWrapper && wrapper && wrapper !== this.lastSelectedWrapper && !e.shiftKey) {
- this.clearSelection();
- this.lastSelectedWrapper = wrapper;
- }
- }
-
- // Shift+click for range selection
+ // Only care about shift-clicks on checkboxes/labels
if (!e.shiftKey) return;
- const checkbox = e.target.closest(this.selectors.checkbox);
- if (!checkbox || !this.lastSelected || !this.lastSelectedWrapper) return;
+ const item = e.target.closest(this.selectors.item.item);
+ if (!item) return;
- if (!item || !wrapper) return;
+ // Check if clicking checkbox or its label
+ const clickedCheckbox = e.target.matches(this.selectors.item.checkbox);
+ const clickedLabel = e.target.closest('label[for]');
- // Range selection only works within the same wrapper
- if (wrapper !== this.lastSelectedWrapper) return;
+ if (!clickedCheckbox && !clickedLabel) return;
- const items = Array.from(wrapper.querySelectorAll(this.selectors.item));
+ const wrapper = this.getItemWrapper(item);
+ if (!wrapper) return;
+ // Can't do range selection without a previous selection in same wrapper
+ if (!this.lastSelected || !this.lastSelectedWrapper || wrapper.element !== this.lastSelectedWrapper) {
+ return; // Let change handler deal with it normally
+ }
+
+ e.preventDefault(); // Stop the checkbox from toggling
+
+ // Do range selection
const currentId = this.getItemId(item);
-
- const lastIndex = items.findIndex(el => this.getItemId(el) === this.lastSelected);
- const currentIndex = items.findIndex(el => this.getItemId(el) === currentId);
+ const items = this.getWrapperChildren(wrapper);
+ const lastIndex = items.findIndex(itemId => itemId === this.lastSelected);
+ const currentIndex = items.findIndex(itemId => itemId === currentId);
if (lastIndex === -1 || currentIndex === -1) return;
@@ -102,106 +150,71 @@
const rangeItems = items.slice(start, end + 1);
rangeItems.forEach(rangeItem => {
- this.select(this.getItemId(rangeItem));
+ this.select(rangeItem,true,false);
});
+ this.lastSelected = currentId;
+ this.updateSelectionUI();
+
this.notify('range-selected', {
selectedItems: new Set(this.selectedItems),
wrapper: wrapper
});
}
+ getWrapperChildren(wrapper) {
+ return Array.from(wrapper.items.children)
+ .map(item => this.getItemId(item));
+ }
+
getItemWrapper(item) {
if (!item) return null;
+ let wrapper = item.closest(this.selectors.wrapper.wrapper);
+ if (!wrapper)return null;
+ return this.getWrapper(wrapper);
+ }
- // Split the compound selector and check each one
- const wrapperSelectors = this.selectors.wrapper.split(',').map(s => s.trim());
-
- for (const selector of wrapperSelectors) {
- const wrapper = item.closest(selector);
- if (wrapper) return wrapper;
- }
-
- return null;
+ getWrapper(wrapper) {
+ return this.ui.wrappers[wrapper.dataset[this.selectors.wrapper.id]]??null;
}
handleKeys(e) {
- // Ctrl/Cmd + A: Select all
- if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
- e.preventDefault();
-
- // If no lastClicked wrapper, clear everything
- if (!this.lastClicked) {
- this.clearSelection();
- if (window.jvbA11y) {
- window.jvbA11y.announce('Selection cleared');
- }
- return;
- }
-
- // First escape: clear items in the current wrapper
- const wrapperItems = this.lastClicked.querySelectorAll(this.selectors.item);
- const wrapperIds = Array.from(wrapperItems).map(item => this.getItemId(item));
- const hadWrapperSelection = wrapperIds.some(id => this.selectedItems.has(id));
-
- if (hadWrapperSelection) {
- // Clear just the wrapper's items
- wrapperIds.forEach(id => this.deselect(id));
-
- // If there are still items selected elsewhere, announce partial clear
- if (this.selectedItems.size > 0) {
- if (window.jvbA11y) {
- window.jvbA11y.announce('Selection cleared in current group');
- }
- } else {
- if (window.jvbA11y) {
- window.jvbA11y.announce('Selection cleared');
- }
- }
- } else {
- // Second escape or no selection in wrapper: clear everything
- this.clearSelection();
- if (window.jvbA11y) {
- window.jvbA11y.announce('All selections cleared');
- }
- }
- }
-
// Escape: Deselect all
if (e.key === 'Escape' && this.selectedItems.size > 0) {
- this.clearSelection();
- if (window.jvbA11y) {
- window.jvbA11y.announce('Selection cleared');
+ e.preventDefault();
+ if (Object.keys(this.ui.wrappers).length > 1 && this.lastClicked) {
+ this.clearWrapperSelection(this.lastClicked);
+ } else {
+ this.clearSelection();
}
}
}
handleSelectAll(trigger) {
- const wrapper = this.getItemWrapper(trigger) || trigger.closest(this.selectors.wrapper);
+ const wrapper = this.getItemWrapper(trigger);
if (!wrapper) return;
// Clear any existing selection from other wrappers first
- if (this.lastSelectedWrapper && wrapper !== this.lastSelectedWrapper) {
- this.clearSelection();
- }
+ //Add this back if it makes more sense to clear between select-alls
+ // if (this.lastSelectedWrapper && wrapper.element !== this.lastSelectedWrapper) {
+ // this.clearSelection();
+ // }
- const items = wrapper.querySelectorAll(this.selectors.item);
- const ids = Array.from(items).map(item => this.getItemId(item));
+ const ids = this.getWrapperChildren(wrapper);
if (trigger.checked) {
ids.forEach(id => this.select(id, true, false));
- this.lastSelectedWrapper = wrapper;
+ this.lastSelectedWrapper = wrapper.element;
} else {
ids.forEach(id => this.deselect(id, true, false));
- if (this.selectedItems.size === 0) {
- this.lastSelectedWrapper = null;
- }
+ this.lastSelectedWrapper = null;
}
- let label = trigger.nextElementSibling || trigger.previousElementSibling;
- if (label && label.tagName === 'LABEL') {
- label.textContent = (trigger.checked && items.length > 0) ? 'Clear Selection' : 'Select All';
+ // Update label text
+ if (wrapper.selectAll.span) {
+ wrapper.selectAll.span.textContent = (trigger.checked && ids.length > 0) ? 'Clear Selection' : 'Select All';
}
+
this.updateSelectionUI();
this.notify('select-all', {
@@ -213,7 +226,11 @@
}
getItemId(item) {
- return item.dataset.uploadId;
+ if (!item instanceof Element) {
+ item = item.element??false;
+ if (!item) return;
+ }
+ return item.dataset[`${this.selectors.item.idAttribute}`];
}
/*******************************************************************
@@ -223,6 +240,8 @@
if (this.selectedItems.has(id)) return;
this.selectedItems.add(id);
+ let item = this.getItem(id);
+ if (item) item.element.classList.add('selected');
if (updateCheckbox) this.setCheckboxState(id, true);
if (updateUI) {
this.updateSelectionUI();
@@ -234,6 +253,8 @@
if (!this.selectedItems.has(id)) return;
this.selectedItems.delete(id);
+ let item = this.getItem(id);
+ if (item) item.element.classList.remove('selected');
if (updateCheckbox) this.setCheckboxState(id, false);
if (updateUI) {
this.updateSelectionUI();
@@ -247,22 +268,34 @@
}
clearSelection() {
- this.selectedItems.forEach(id => this.setCheckboxState(id, false));
+ this.selectedItems.forEach(id => this.deselect(id,true,false));
this.selectedItems.clear();
this.lastSelected = null;
this.lastSelectedWrapper = null;
// Uncheck all select-all triggers
- this.container.querySelectorAll(this.selectors.selectAll).forEach(trigger => {
- trigger.checked = false;
- const label = trigger.nextElementSibling || trigger.previousElementSibling;
- if (label?.tagName === 'LABEL') {
- label.textContent = 'Select All';
+ for (let wrapper of Object.values(this.ui.wrappers)) {
+ if (wrapper.selectAll.checkbox) wrapper.selectAll.checkbox.checked = false;
+ if (wrapper.selectAll.span) {
+ wrapper.selectAll.span.textContent = 'Select All';
}
- });
+ }
+ this.a11y.announce('Selection cleared');
this.updateSelectionUI();
this.notify('selection-cleared', { selectedItems: new Set() });
}
+ clearWrapperSelection(wrapper) {
+ wrapper = this.getWrapper(wrapper);
+ if(!wrapper) return;
+ this.getWrapperChildren(wrapper).forEach(id => this.deselect(id, true, false));
+ if (wrapper.selectAll.checkbox) wrapper.selectAll.checkbox.checked = false;
+ if (wrapper.selectAll.span) {
+ wrapper.selectAll.span.textContent = 'Select All';
+ }
+ this.a11y.announce('Selection cleared in group');
+ this.updateSelectionUI();
+ this.notify('wrapper-selection-cleared', {selectedItems: this.selectedItems});
+ }
isSelected(id) {
return this.selectedItems.has(id);
@@ -276,32 +309,67 @@
* DOM HELPERS
*******************************************************************/
setCheckboxState(id, checked) {
- const item = this.container.querySelector(`[data-upload-id="${id}"]`);
- const checkbox = item?.querySelector(this.selectors.checkbox);
- if (checkbox && checkbox.checked !== checked) {
- checkbox.checked = checked;
+ const item = this.getItem(id);
+ if (!item || !item.checkbox) return;
+
+ if (item.checkbox.checked !== checked) {
+ item.checkbox.checked = checked;
}
}
updateSelectionUI() {
- if (!this.lastClicked || !this.ui.count) return;
+ if (!this.lastClicked) return;
+
+ const wrapper = this.getWrapper(this.lastClicked);
+ if (!wrapper || !wrapper.selectAll) return;
const count = this.selectedItems.size;
- // Update bulk controls visibility
- let controls = this.lastClicked.querySelector(this.selectors.bulkControls);
+ // Fix property paths:
+ let controls = wrapper.selectAll.bulkControls;
if (controls) {
controls.hidden = count === 0;
}
- // Update count display
- let countEl = this.lastClicked.querySelector(this.selectors.count);
+ let countEl = wrapper.selectAll.count;
if (countEl) {
const itemText = count === 1 ? 'item' : 'items';
- countEl.textContent = count === 0 ? '' : `{ ${count} ${itemText} selected }`;
+ countEl.textContent = count === 0 ? '' : `${count} ${itemText} selected`;
countEl.hidden = count === 0;
}
}
/*******************************************************************
+ ITEM DOM CACHING
+ *******************************************************************/
+ collectItems() {
+ this.container.querySelectorAll(this.selectors.item.item).forEach(item => {
+ this.setItem(item, true);
+ });
+ }
+
+ getItem(id) {
+ if (this.items.has(id)) return this.items.get(id);
+ return this.setItem(id);
+ }
+ setItem(id, isElement = false) {
+ let element = (!isElement)
+ ? this.container.querySelector(`[data-${this.camelToKebab(this.selectors.item.idAttribute)}="${id}"]`)
+ : id;
+ if (!element) return null;
+
+ id = this.getItemId(element);
+ if (this.items.has(id)) return this.items.get(id);
+
+ this.items.set(id, {
+ element: element,
+ checkbox: element.querySelector(this.selectors.item.checkbox)
+ });
+ return this.items.get(id);
+ }
+
+ camelToKebab(str) {
+ return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
+ }
+ /*******************************************************************
* EVENT SYSTEM
*******************************************************************/
subscribe(callback) {
--
Gitblit v1.10.0