multi-select.js (6.6 KB)
1 // Multi-select: state management and bulk operations 2 3 function clearMultiSelect() { 4 state.multiSelectAnchorId = null; 5 state.multiSelectedIds = []; 6 var focused = document.activeElement; 7 var focusedContainer = focused ? focused.closest('.task-container') : null; 8 document.querySelectorAll('.task-container.active').forEach(el => { 9 if (el !== focusedContainer) el.classList.remove('active'); 10 }); 11 } 12 13 function getMultiSelectedTasks() { 14 return state.currentTask.subtasks.filter(t => state.multiSelectedIds.includes(t.id)); 15 } 16 17 function applyMultiSelectHighlights() { 18 for (var id of state.multiSelectedIds) { 19 var container = document.querySelector(`.task-container[data-id="${id}"]`); 20 if (container) container.classList.add('active'); 21 } 22 } 23 24 function shakeAllSelected(direction = 'horizontal') { 25 for (var id of state.multiSelectedIds) { 26 applyShakeAnimation(id, direction); 27 } 28 } 29 30 function extendMultiSelect(task, direction) { 31 if (task === state.currentTask) return; 32 33 var tasks = state.currentTask.subtasks; 34 if (tasks.length === 0) return; 35 36 if (state.multiSelectAnchorId === null) { 37 state.multiSelectAnchorId = task.id; 38 state.multiSelectedIds = [task.id]; 39 } 40 41 var anchorIndex = tasks.findIndex(t => t.id === state.multiSelectAnchorId); 42 var currentIndex = tasks.findIndex(t => t.id === task.id); 43 44 if (direction === 'down') { 45 var bottomIndex = tasks.findIndex((t, i) => { 46 return state.multiSelectedIds.includes(t.id) && 47 (i === tasks.length - 1 || !state.multiSelectedIds.includes(tasks[i + 1].id)); 48 }); 49 if (currentIndex <= anchorIndex && state.multiSelectedIds.length > 1) { 50 state.multiSelectedIds = state.multiSelectedIds.filter(id => id !== task.id); 51 var nextTask = tasks[currentIndex + 1]; 52 if (nextTask) selectAndFocusTask(nextTask); 53 } else if (bottomIndex < tasks.length - 1) { 54 var nextTask = tasks[bottomIndex + 1]; 55 state.multiSelectedIds.push(nextTask.id); 56 selectAndFocusTask(nextTask); 57 } 58 } else { 59 var topIndex = tasks.findIndex(t => state.multiSelectedIds.includes(t.id)); 60 if (currentIndex >= anchorIndex && state.multiSelectedIds.length > 1) { 61 state.multiSelectedIds = state.multiSelectedIds.filter(id => id !== task.id); 62 var prevTask = tasks[currentIndex - 1]; 63 if (prevTask) selectAndFocusTask(prevTask); 64 } else if (topIndex > 0) { 65 var prevTask = tasks[topIndex - 1]; 66 state.multiSelectedIds.unshift(prevTask.id); 67 selectAndFocusTask(prevTask); 68 } 69 } 70 71 applyMultiSelectHighlights(); 72 } 73 74 function moveMultiSelected(direction) { 75 var selected = getMultiSelectedTasks(); 76 if (selected.length === 0) return false; 77 78 var subtasks = state.currentTask.subtasks; 79 var indices = selected.map(t => subtasks.findIndex(s => s.id === t.id)).sort((a, b) => a - b); 80 var topIndex = indices[0]; 81 var bottomIndex = indices[indices.length - 1]; 82 83 if (direction === 'up' && topIndex <= 0) return false; 84 if (direction === 'down' && bottomIndex >= subtasks.length - 1) return false; 85 86 var chunk = subtasks.splice(topIndex, selected.length); 87 var insertAt = direction === 'up' ? topIndex - 1 : topIndex + 1; 88 subtasks.splice(insertAt, 0, ...chunk); 89 90 state.currentTask.selectedSubtaskId = state.multiSelectAnchorId; 91 renderCurrentView(); 92 applyMultiSelectHighlights(); 93 scheduleSave(); 94 return true; 95 } 96 97 function pushMultiSelectedIntoTarget(direction, navigate = false) { 98 var selected = getMultiSelectedTasks(); 99 if (selected.length === 0) return false; 100 101 if (navigate) { 102 document.documentElement.style.scrollBehavior = 'auto'; 103 } 104 105 var subtasks = state.currentTask.subtasks; 106 var indices = selected.map(t => subtasks.findIndex(s => s.id === t.id)).sort((a, b) => a - b); 107 var topIndex = indices[0]; 108 var bottomIndex = indices[indices.length - 1]; 109 110 var targetTask = null; 111 if (direction === 'up' && topIndex > 0) { 112 targetTask = subtasks[topIndex - 1]; 113 } else if (direction === 'down' && bottomIndex < subtasks.length - 1) { 114 targetTask = subtasks[bottomIndex + 1]; 115 } 116 if (!targetTask) return false; 117 118 var chunk = selected.slice(); 119 state.currentTask.subtasks = state.currentTask.subtasks.filter(t => !state.multiSelectedIds.includes(t.id)); 120 121 targetTask.subtasks.unshift(...chunk); 122 123 for (var t of chunk) { 124 adjustMovedTaskState(t, targetTask); 125 } 126 targetTask.selectedSubtaskId = state.multiSelectAnchorId; 127 updateTaskAndAncestors(state.currentTask); 128 updateTaskAndAncestors(targetTask); 129 130 if (navigate) { 131 state.currentTask.selectedSubtaskId = targetTask.id; 132 state.taskPath.push(targetTask); 133 state.currentTask = targetTask; 134 updateBreadcrumbs(state.currentTask); 135 renderCurrentView(); 136 applyMultiSelectHighlights(); 137 } else { 138 clearMultiSelect(); 139 renderCurrentView(); 140 selectAndFocusTask(targetTask); 141 } 142 143 scheduleSave(); 144 return true; 145 } 146 147 function pullMultiSelectedOutLayer(navigate = false) { 148 if (state.taskPath.length <= 1) return false; 149 150 var selected = getMultiSelectedTasks(); 151 if (selected.length === 0) return false; 152 153 if (navigate) { 154 document.documentElement.style.scrollBehavior = 'auto'; 155 } 156 157 var currentParent = state.currentTask; 158 var grandParent = state.taskPath[state.taskPath.length - 2]; 159 if (!grandParent) return false; 160 161 currentParent.subtasks = currentParent.subtasks.filter(t => !state.multiSelectedIds.includes(t.id)); 162 163 var parentIndex = grandParent.subtasks.findIndex(t => t.id === currentParent.id); 164 if (parentIndex === -1) { 165 grandParent.subtasks.push(...selected); 166 } else { 167 grandParent.subtasks.splice(parentIndex + 1, 0, ...selected); 168 } 169 170 for (var t of selected) { 171 adjustMovedTaskState(t, grandParent); 172 } 173 174 updateTaskAndAncestors(currentParent); 175 updateTaskAndAncestors(grandParent); 176 177 if (navigate || currentParent.subtasks.length === 0) { 178 state.taskPath.pop(); 179 state.currentTask = state.taskPath[state.taskPath.length - 1]; 180 state.currentTask.selectedSubtaskId = state.multiSelectAnchorId; 181 updateBreadcrumbs(state.currentTask); 182 renderCurrentView(); 183 applyMultiSelectHighlights(); 184 } else { 185 clearMultiSelect(); 186 renderCurrentView(); 187 if (currentParent.subtasks.length > 0) { 188 selectAndFocusTask(currentParent.subtasks[0]); 189 } else { 190 selectAndFocusTask(currentParent); 191 } 192 } 193 194 scheduleSave(); 195 return true; 196 } 197 198 function bulkToggleTaskState() { 199 var selected = getMultiSelectedTasks(); 200 if (selected.length === 0) return; 201 202 var anyUnchecked = selected.some(t => t.state !== 1); 203 var newState = anyUnchecked ? 1 : 0; 204 205 for (var t of selected) { 206 function setAllSubtasks(task, s) { 207 task.state = s; 208 for (var sub of task.subtasks) { 209 setAllSubtasks(sub, s); 210 } 211 } 212 setAllSubtasks(t, newState); 213 } 214 215 updateTaskAndAncestors(state.currentTask); 216 state.currentTask.selectedSubtaskId = state.multiSelectAnchorId; 217 renderCurrentView(); 218 applyMultiSelectHighlights(); 219 scheduleSave(); 220 }