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 }