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 }