Skip to main content

max / goingson

6.7 KB · 221 lines History Blame Raw
1 /**
2 * GoingsOn - Selection Manager
3 * Generic selection handling with shift-click range selection support.
4 * Replaces duplicate selection code in tasks.js and emails.js.
5 *
6 * Supports both DOM-based and data-based range selection for virtual scrolling.
7 */
8
9 (function() {
10 'use strict';
11
12 class SelectionManager {
13 /**
14 * Create a selection manager.
15 * @param {string} type - Item type ('task' or 'email')
16 * @param {string} containerSelector - CSS selector for the container with checkboxes
17 * @param {string} bulkBarId - ID of the bulk actions bar element
18 */
19 constructor(type, containerSelector, bulkBarId) {
20 this.type = type;
21 this.containerSelector = containerSelector;
22 this.bulkBarId = bulkBarId;
23 this.selectedIds = new Set();
24 this.lastClickedIndex = -1;
25 this.lastClickedId = null;
26 this.items = []; // For data-based range selection
27 }
28
29 /**
30 * Set the current items array for data-based range selection.
31 * Call this before rendering when using virtual scrolling.
32 * @param {Array} items - Array of items with id property
33 */
34 setItems(items) {
35 this.items = items || [];
36 }
37
38 /**
39 * Toggle selection for an item. Supports shift-click for range selection.
40 * Works with both DOM-based and data-based approaches.
41 * @param {string} id - Item ID
42 * @param {HTMLInputElement} checkbox - The checkbox element
43 * @param {Event} event - The click event (for shift key detection)
44 */
45 toggle(id, checkbox, event) {
46 // Find current index - prefer data-based if items are set
47 let currentIndex;
48 if (this.items.length > 0) {
49 currentIndex = this.items.findIndex(item => item.id === id);
50 } else {
51 const checkboxes = Array.from(
52 document.querySelectorAll(`${this.containerSelector} .bulk-checkbox`)
53 );
54 currentIndex = checkboxes.findIndex(cb => cb.dataset.id === id);
55 }
56
57 // Shift-click for range selection
58 if (event && event.shiftKey && this.lastClickedIndex !== -1 && currentIndex !== -1) {
59 const start = Math.min(this.lastClickedIndex, currentIndex);
60 const end = Math.max(this.lastClickedIndex, currentIndex);
61 const shouldSelect = checkbox.checked;
62
63 if (this.items.length > 0) {
64 // Data-based range selection (for virtual scrolling)
65 for (let i = start; i <= end; i++) {
66 const item = this.items[i];
67 if (item && item.id) {
68 if (shouldSelect) {
69 this.selectedIds.add(item.id);
70 } else {
71 this.selectedIds.delete(item.id);
72 }
73 }
74 }
75 // Update visible checkboxes
76 this._syncVisibleCheckboxes();
77 } else {
78 // DOM-based range selection (legacy)
79 const checkboxes = Array.from(
80 document.querySelectorAll(`${this.containerSelector} .bulk-checkbox`)
81 );
82 for (let i = start; i <= end; i++) {
83 const cb = checkboxes[i];
84 if (cb) {
85 cb.checked = shouldSelect;
86 if (shouldSelect) {
87 this.selectedIds.add(cb.dataset.id);
88 } else {
89 this.selectedIds.delete(cb.dataset.id);
90 }
91 }
92 }
93 }
94 } else {
95 // Normal click
96 if (checkbox.checked) {
97 this.selectedIds.add(id);
98 } else {
99 this.selectedIds.delete(id);
100 }
101 }
102
103 this.lastClickedIndex = currentIndex;
104 this.lastClickedId = id;
105 this.updateBulkActionsBar();
106
107 // One-time hint about shift-click range selection
108 if (this.selectedIds.size === 1 && GoingsOn.app?.showHint) {
109 GoingsOn.app.showHint('go-hint-shift-select', 'Shift-click to select a range of items');
110 }
111 }
112
113 /**
114 * Sync visible checkbox states with selectedIds.
115 * Used after data-based range selection.
116 * @private
117 */
118 _syncVisibleCheckboxes() {
119 const checkboxes = document.querySelectorAll(`${this.containerSelector} .bulk-checkbox`);
120 checkboxes.forEach(cb => {
121 cb.checked = this.selectedIds.has(cb.dataset.id);
122 });
123 }
124
125 /**
126 * Select or deselect all items.
127 */
128 selectAll() {
129 const checkboxes = document.querySelectorAll(`${this.containerSelector} .bulk-checkbox`);
130 const allSelected = this.selectedIds.size === checkboxes.length && checkboxes.length > 0;
131
132 checkboxes.forEach(cb => {
133 cb.checked = !allSelected;
134 const id = cb.dataset.id;
135 if (!allSelected) {
136 this.selectedIds.add(id);
137 } else {
138 this.selectedIds.delete(id);
139 }
140 });
141
142 this.updateBulkActionsBar();
143 }
144
145 /**
146 * Get the set of selected IDs.
147 * @returns {Set<string>}
148 */
149 getSelected() {
150 return this.selectedIds;
151 }
152
153 /**
154 * Check if any items are selected.
155 * @returns {boolean}
156 */
157 hasSelection() {
158 return this.selectedIds.size > 0;
159 }
160
161 /**
162 * Get the count of selected items.
163 * @returns {number}
164 */
165 getCount() {
166 return this.selectedIds.size;
167 }
168
169 /**
170 * Clear all selections.
171 */
172 clear() {
173 this.selectedIds.clear();
174 this.lastClickedIndex = -1;
175
176 // Uncheck all checkboxes
177 const checkboxes = document.querySelectorAll(`${this.containerSelector} .bulk-checkbox`);
178 checkboxes.forEach(cb => {
179 cb.checked = false;
180 });
181
182 this.updateBulkActionsBar();
183 }
184
185 /**
186 * Update the bulk actions bar visibility and count.
187 */
188 updateBulkActionsBar() {
189 const bar = document.getElementById(this.bulkBarId);
190 if (!bar) return;
191
192 const count = this.selectedIds.size;
193 if (count > 0) {
194 bar.classList.remove('hidden');
195 const countEl = bar.querySelector('.bulk-count');
196 if (countEl) {
197 countEl.textContent = `${count} selected`;
198 }
199 } else {
200 bar.classList.add('hidden');
201 }
202 }
203
204 /**
205 * Check if an item is selected.
206 * @param {string} id - Item ID
207 * @returns {boolean}
208 */
209 isSelected(id) {
210 return this.selectedIds.has(id);
211 }
212 }
213
214 // ============ Populate GoingsOn Namespace ============
215
216 if (window.GoingsOn) {
217 GoingsOn.SelectionManager = SelectionManager;
218 }
219
220 })();
221