Skip to main content

max / goingson

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