/** * GoingsOn - Selection Manager * Generic selection handling with shift-click range selection support. * Replaces duplicate selection code in tasks.js and emails.js. * * Supports both DOM-based and data-based range selection for virtual scrolling. */ class SelectionManager { /** * Create a selection manager. * @param {string} type - Item type ('task' or 'email') * @param {string} containerSelector - CSS selector for the container with checkboxes * @param {string} bulkBarId - ID of the bulk actions bar element */ constructor(type, containerSelector, bulkBarId) { this.type = type; this.containerSelector = containerSelector; this.bulkBarId = bulkBarId; this.selectedIds = new Set(); this.lastClickedIndex = -1; this.lastClickedId = null; this.items = []; // For data-based range selection } /** * Set the current items array for data-based range selection. * Call this before rendering when using virtual scrolling. * @param {Array} items - Array of items with id property */ setItems(items) { this.items = items || []; } /** * Toggle selection for an item. Supports shift-click for range selection. * Works with both DOM-based and data-based approaches. * @param {string} id - Item ID * @param {HTMLInputElement} checkbox - The checkbox element * @param {Event} event - The click event (for shift key detection) */ toggle(id, checkbox, event) { // Find current index - prefer data-based if items are set let currentIndex; if (this.items.length > 0) { currentIndex = this.items.findIndex(item => item.id === id); } else { const checkboxes = Array.from( document.querySelectorAll(`${this.containerSelector} .bulk-checkbox`) ); currentIndex = checkboxes.findIndex(cb => cb.dataset.id === id); } // Shift-click for range selection if (event && event.shiftKey && this.lastClickedIndex !== -1 && currentIndex !== -1) { const start = Math.min(this.lastClickedIndex, currentIndex); const end = Math.max(this.lastClickedIndex, currentIndex); const shouldSelect = checkbox.checked; if (this.items.length > 0) { // Data-based range selection (for virtual scrolling) for (let i = start; i <= end; i++) { const item = this.items[i]; if (item && item.id) { if (shouldSelect) { this.selectedIds.add(item.id); } else { this.selectedIds.delete(item.id); } } } // Update visible checkboxes this._syncVisibleCheckboxes(); } else { // DOM-based range selection (legacy) const checkboxes = Array.from( document.querySelectorAll(`${this.containerSelector} .bulk-checkbox`) ); for (let i = start; i <= end; i++) { const cb = checkboxes[i]; if (cb) { cb.checked = shouldSelect; if (shouldSelect) { this.selectedIds.add(cb.dataset.id); } else { this.selectedIds.delete(cb.dataset.id); } } } } } else { // Normal click if (checkbox.checked) { this.selectedIds.add(id); } else { this.selectedIds.delete(id); } } this.lastClickedIndex = currentIndex; this.lastClickedId = id; this.updateBulkActionsBar(); // One-time hint about shift-click range selection if (this.selectedIds.size === 1 && GoingsOn.app?.showHint) { GoingsOn.app.showHint('go-hint-shift-select', 'Shift-click to select a range of items'); } } /** * Sync visible checkbox states with selectedIds. * Used after data-based range selection. * @private */ _syncVisibleCheckboxes() { const checkboxes = document.querySelectorAll(`${this.containerSelector} .bulk-checkbox`); checkboxes.forEach(cb => { cb.checked = this.selectedIds.has(cb.dataset.id); }); } /** * Select or deselect all items. */ selectAll() { const checkboxes = document.querySelectorAll(`${this.containerSelector} .bulk-checkbox`); const allSelected = this.selectedIds.size === checkboxes.length && checkboxes.length > 0; checkboxes.forEach(cb => { cb.checked = !allSelected; const id = cb.dataset.id; if (!allSelected) { this.selectedIds.add(id); } else { this.selectedIds.delete(id); } }); this.updateBulkActionsBar(); } /** * Get the set of selected IDs. * @returns {Set} */ getSelected() { return this.selectedIds; } /** * Check if any items are selected. * @returns {boolean} */ hasSelection() { return this.selectedIds.size > 0; } /** * Get the count of selected items. * @returns {number} */ getCount() { return this.selectedIds.size; } /** * Clear all selections. */ clear() { this.selectedIds.clear(); this.lastClickedIndex = -1; // Uncheck all checkboxes const checkboxes = document.querySelectorAll(`${this.containerSelector} .bulk-checkbox`); checkboxes.forEach(cb => { cb.checked = false; }); this.updateBulkActionsBar(); } /** * Update the bulk actions bar visibility and count. */ updateBulkActionsBar() { const bar = document.getElementById(this.bulkBarId); if (!bar) return; const count = this.selectedIds.size; if (count > 0) { bar.classList.remove('hidden'); const countEl = bar.querySelector('.bulk-count'); if (countEl) { countEl.textContent = `${count} selected`; } } else { bar.classList.add('hidden'); } } /** * Check if an item is selected. * @param {string} id - Item ID * @returns {boolean} */ isSelected(id) { return this.selectedIds.has(id); } } // ============ Populate GoingsOn Namespace ============ if (window.GoingsOn) { GoingsOn.SelectionManager = SelectionManager; }