task-model.js (4.9 KB)


  1 // Task model: state calculations, checkbox logic, tree traversal
  2 
  3 function updateCheckboxState(checkbox, taskState) {
  4 	checkbox.checked = taskState === 1;
  5 	checkbox.indeterminate = taskState === 2;
  6 }
  7 
  8 function toggleTaskState(task) {
  9 	// Save cursor position before re-rendering
 10 	var taskInput = document.querySelector(`.task-container[data-id="${task.id}"] input[type="text"]`);
 11 	var cursorPos = taskInput ? taskInput.selectionStart : null;
 12 
 13 	if (task.state === 1) {
 14 		if (task.subtasks.length === 0 || !task.subtasks.every(t => t.state === 1)) {
 15 			task.state = 0;
 16 			updateSubtasksState(task, 0);
 17 
 18 			var parent = findParentTask(task);
 19 			if (parent) {
 20 				updateTaskAndAncestors(parent);
 21 			}
 22 
 23 			renderCurrentView();
 24 			selectAndFocusTask(task, cursorPos);
 25 			scheduleSave();
 26 		} else {
 27 			applyShakeAnimation(task.id);
 28 		}
 29 	} else {
 30 		task.state = 1;
 31 		updateSubtasksState(task, 1);
 32 
 33 		var parent = findParentTask(task);
 34 		if (parent) {
 35 			updateTaskAndAncestors(parent);
 36 		}
 37 
 38 		renderCurrentView();
 39 		selectAndFocusTask(task, cursorPos);
 40 		scheduleSave();
 41 	}
 42 }
 43 
 44 function recalculateTaskState(task) {
 45 	if (task.subtasks.length === 0) {
 46 		return task.state;
 47 	}
 48 	var anyUnchecked = task.subtasks.some(t => t.state === 0);
 49 	var allChecked = task.subtasks.every(t => t.state === 1);
 50 	var allCheckedOrIndeterminate = task.subtasks.every(t => t.state === 1 || t.state === 2);
 51 
 52 	if (allChecked) {
 53 		return 1;
 54 	} else if (allCheckedOrIndeterminate) {
 55 		return task.state === 2 ? 2 : 1;
 56 	} else if (anyUnchecked) {
 57 		return 0;
 58 	}
 59 }
 60 
 61 function updateTaskAndAncestors(task) {
 62 	var newState = recalculateTaskState(task);
 63 	if (task.state !== newState) {
 64 		var oldState = task.state;
 65 		task.state = newState;
 66 
 67 		if (oldState === 1 && newState === 0) {
 68 			updateSubtasksState(task, 0);
 69 		}
 70 
 71 		var parent = findParentTask(task);
 72 		if (parent) {
 73 			updateTaskAndAncestors(parent);
 74 		}
 75 	}
 76 }
 77 
 78 function updateSubtasksState(task, newState) {
 79 	task.subtasks.forEach(subtask => {
 80 		if (subtask.state !== 1) {
 81 			if (newState === 1) {
 82 				subtask.state = subtask.state === 0 ? 2 : subtask.state;
 83 			} else if (newState === 0) {
 84 				subtask.state = 0;
 85 			}
 86 			if (subtask.subtasks.length > 0) {
 87 				updateSubtasksState(subtask, newState);
 88 			}
 89 		}
 90 	});
 91 }
 92 
 93 function adjustMovedTaskState(movedTask, newParent) {
 94 	if (movedTask.state === 2 && newParent.state === 0) {
 95 		movedTask.state = 0;
 96 		updateSubtasksState(movedTask, 0);
 97 	}
 98 }
 99 
100 function findParentTask(task) {
101 	for (var i = state.taskPath.length - 1; i >= 0; i--) {
102 		var potentialParent = state.taskPath[i];
103 		if (potentialParent.subtasks.some(t => t.id === task.id)) {
104 			return potentialParent;
105 		}
106 	}
107 	return null;
108 }
109 
110 function isLastSubtask(task) {
111 	var parentTask = findParentTask(task);
112 	if (!parentTask) return false;
113 	return parentTask.subtasks[parentTask.subtasks.length - 1].id === task.id;
114 }
115 
116 function addNewSubtask(parentTask, currentSubtask = null) {
117 	var newSubtask = { id: generateId(), text: '', state: 0, subtasks: [], selectedSubtaskId: null };
118 	if (currentSubtask) {
119 		var index = parentTask.subtasks.findIndex(t => t.id === currentSubtask.id);
120 		parentTask.subtasks.splice(index + 1, 0, newSubtask);
121 	} else {
122 		parentTask.subtasks.push(newSubtask);
123 	}
124 
125 	updateTaskAndAncestors(parentTask);
126 	renderCurrentView();
127 	selectAndFocusTask(newSubtask);
128 	scheduleSave();
129 }
130 
131 function deleteSubtask(subtask) {
132 	var parentTask = state.taskPath[state.taskPath.length - 1];
133 	var index = parentTask.subtasks.findIndex(t => t.id === subtask.id);
134 
135 	if (parentTask.id === 'root' && parentTask.subtasks.length === 1) {
136 		applyShakeAnimation(subtask.id);
137 		return;
138 	}
139 
140 	parentTask.subtasks = parentTask.subtasks.filter(t => t.id !== subtask.id);
141 	updateTaskAndAncestors(parentTask);
142 
143 	if (parentTask.subtasks.length === 0 && state.taskPath.length > 1) {
144 		navigateToParentTask();
145 	} else {
146 		renderCurrentView();
147 		if (parentTask.subtasks.length > 0) {
148 			var targetIndex = Math.max(0, index - 1);
149 			selectAndFocusTask(parentTask.subtasks[targetIndex]);
150 		} else {
151 			selectAndFocusTask(parentTask);
152 		}
153 	}
154 	scheduleSave();
155 }
156 
157 function deleteCurrentParentTask() {
158 	if (state.taskPath.length <= 1) return;
159 
160 	var currentParentTask = state.taskPath[state.taskPath.length - 1];
161 	var grandparentTask = state.taskPath[state.taskPath.length - 2];
162 
163 	if (grandparentTask.id === 'root' && grandparentTask.subtasks.length === 1) {
164 		applyShakeAnimation(currentParentTask.id);
165 		return;
166 	}
167 
168 	var index = grandparentTask.subtasks.findIndex(t => t.id === currentParentTask.id);
169 
170 	grandparentTask.subtasks = grandparentTask.subtasks.filter(t => t.id !== currentParentTask.id);
171 	updateTaskAndAncestors(grandparentTask);
172 
173 	state.taskPath.pop();
174 	state.currentTask = grandparentTask;
175 
176 	if (grandparentTask.subtasks.length === 0 && state.taskPath.length > 1) {
177 		navigateToParentTask();
178 	} else {
179 		renderCurrentView();
180 		if (grandparentTask.subtasks.length > 0) {
181 			var targetIndex = Math.max(0, index - 1);
182 			selectAndFocusTask(grandparentTask.subtasks[targetIndex]);
183 		} else {
184 			selectAndFocusTask(grandparentTask);
185 		}
186 	}
187 }