initial commit
Hunter
Thursday, July 25th, 2024 at 11:07 PM
1 +<!DOCTYPE html>
2 +<html lang="en">
3 +<head>
4 + <meta charset="UTF-8">
5 + <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 + <title>Advanced Task Tracker</title>
7 + <style>
8 + body {
9 + font-family: Arial, sans-serif;
10 + max-width: 800px;
11 + margin: 0 auto;
12 + padding: 20px;
13 + }
14 + ul {
15 + list-style-type: none;
16 + padding-left: 20px;
17 + }
18 + input[type="text"] {
19 + border: none;
20 + background: transparent;
21 + font-size: 1.17em;
22 + font-weight: bold;
23 + width: calc(100% - 30px);
24 + margin-left: 5px;
25 + padding: 5px;
26 + }
27 + input[type="text"]:focus {
28 + outline: none;
29 + }
30 + .task-container {
31 + display: flex;
32 + align-items: center;
33 + margin: 5px 0;
34 + }
35 + .active {
36 + background-color: #e6f3ff;
37 + }
38 + .parent-task {
39 + font-size: 1.5em;
40 + font-weight: bold;
41 + }
42 + </style>
43 + <script>
44 + document.addEventListener('DOMContentLoaded', function() {
45 + const appContainer = document.getElementById('app-container');
46 + let currentTask = { id: 'root', text: 'todo', state: 0, subtasks: [{ id: Date.now(), text: '', state: 0, subtasks: [] }], selectedSubtaskId: null };
47 + let taskPath = [currentTask];
48 +
49 + function createTaskElement(task, isParent = false) {
50 + const taskContainer = document.createElement('div');
51 + taskContainer.className = 'task-container';
52 + taskContainer.dataset.id = task.id;
53 + if (isParent) taskContainer.classList.add('parent-task');
54 +
55 + const checkbox = document.createElement('input');
56 + checkbox.type = 'checkbox';
57 + updateCheckboxState(checkbox, task.state);
58 + checkbox.addEventListener('click', (e) => {
59 + e.preventDefault(); // Prevent default checkbox behavior
60 + toggleTaskState(task);
61 + });
62 +
63 + const taskInput = document.createElement('input');
64 + taskInput.type = 'text';
65 + taskInput.value = task.text;
66 + taskInput.addEventListener('keydown', (e) => handleKeyDown(e, task));
67 + taskInput.addEventListener('input', () => task.text = taskInput.value);
68 + taskInput.addEventListener('focus', () => setActiveTask(taskInput, task));
69 +
70 + taskContainer.appendChild(checkbox);
71 + taskContainer.appendChild(taskInput);
72 +
73 + return taskContainer;
74 + }
75 +
76 + function updateCheckboxState(checkbox, state) {
77 + checkbox.checked = state === 1;
78 + checkbox.indeterminate = state === 2;
79 + }
80 +
81 + function toggleTaskState(task) {
82 + if (task.state === 1) {
83 + // If the task is checked, only uncheck it if it's not a parent with all checked direct children
84 + if (task.subtasks.length === 0 || !task.subtasks.every(t => t.state === 1)) {
85 + task.state = 0;
86 + if (task.subtasks.length > 0) {
87 + // If it's a parent task, set indeterminate subtasks to unchecked
88 + task.subtasks.forEach(subtask => {
89 + if (subtask.state === 2) {
90 + subtask.state = 0;
91 + }
92 + });
93 + }
94 + }
95 + // If it's a parent with all direct children checked, do nothing (stay checked)
96 + } else {
97 + // If the task is unchecked or indeterminate, check it
98 + task.state = 1;
99 + if (task.subtasks.length > 0) {
100 + // If it's a parent task, set unchecked subtasks to indeterminate
101 + task.subtasks.forEach(subtask => {
102 + if (subtask.state === 0) {
103 + subtask.state = 2; // Set to indeterminate
104 + }
105 + });
106 + }
107 + }
108 + updateParentState(task);
109 + renderCurrentView();
110 + selectAndFocusTask(task);
111 + }
112 +
113 + function updateParentState(task) {
114 + const parent = findParentTask(task);
115 + if (parent) {
116 + const allChecked = parent.subtasks.every(t => t.state === 1);
117 + const anyUnchecked = parent.subtasks.some(t => t.state === 0);
118 +
119 + if (allChecked) {
120 + parent.state = 1; // Checked
121 + } else if (anyUnchecked) {
122 + parent.state = 0; // Unchecked
123 + }
124 + // If all children are either checked or indeterminate, don't change the parent's state
125 +
126 + updateParentState(parent);
127 + }
128 + }
129 +
130 + function updateSubtasksState(task, state) {
131 + task.subtasks.forEach(subtask => {
132 + subtask.state = state;
133 + if (subtask.subtasks.length > 0) {
134 + updateSubtasksState(subtask, state);
135 + }
136 + });
137 + }
138 +
139 + function findParentTask(task) {
140 + for (let i = taskPath.length - 1; i >= 0; i--) {
141 + const potentialParent = taskPath[i];
142 + if (potentialParent.subtasks.some(t => t.id === task.id)) {
143 + return potentialParent;
144 + }
145 + }
146 + return null;
147 + }
148 +
149 + function handleKeyDown(e, task) {
150 + if (e.key === 'Enter') {
151 + e.preventDefault();
152 + if (e.shiftKey) {
153 + toggleTaskState(task);
154 + } else {
155 + addNewTask(currentTask, task);
156 + }
157 + } else if (e.key === 'Backspace' && e.target.value === '' && task !== currentTask) {
158 + e.preventDefault();
159 + deleteTask(task);
160 + } else if (e.key === 'ArrowUp') {
161 + e.preventDefault();
162 + navigateTasks('up');
163 + } else if (e.key === 'ArrowDown') {
164 + e.preventDefault();
165 + navigateTasks('down');
166 + } else if (e.key === 'ArrowRight' && e.shiftKey) {
167 + e.preventDefault();
168 + if (task !== currentTask) {
169 + navigateInto(task);
170 + }
171 + } else if (e.key === 'ArrowLeft' && e.shiftKey) {
172 + e.preventDefault();
173 + navigateOut();
174 + }
175 + }
176 +
177 + function addNewTask(parentTask, currentSubtask = null) {
178 + const newTask = { id: Date.now(), text: '', state: 0, subtasks: [], selectedSubtaskId: null };
179 + if (currentSubtask) {
180 + const index = parentTask.subtasks.findIndex(t => t.id === currentSubtask.id);
181 + parentTask.subtasks.splice(index + 1, 0, newTask);
182 + } else {
183 + parentTask.subtasks.push(newTask);
184 + }
185 +
186 + parentTask.state = 0;
187 + parentTask.subtasks.forEach(subtask => {
188 + if (subtask.state === 2) {
189 + subtask.state = 0;
190 + }
191 + });
192 +
193 + renderCurrentView();
194 + selectAndFocusTask(newTask);
195 + }
196 +
197 + function selectAndFocusTask(task) {
198 + const taskInput = document.querySelector(`.task-container[data-id="${task.id}"] input[type="text"]`);
199 + if (taskInput) {
200 + taskInput.focus();
201 + setActiveTask(taskInput, task);
202 + }
203 + }
204 +
205 + function deleteTask(task) {
206 + const parent = taskPath[taskPath.length - 1];
207 + const index = parent.subtasks.findIndex(t => t.id === task.id);
208 +
209 + if (parent.id === 'root' && parent.subtasks.length === 1) {
210 + return;
211 + }
212 +
213 + parent.subtasks = parent.subtasks.filter(t => t.id !== task.id);
214 + updateParentState(parent);
215 +
216 + if (parent.subtasks.length === 0 && taskPath.length > 1) {
217 + navigateOut();
218 + } else {
219 + renderCurrentView();
220 + if (parent.subtasks.length > 0) {
221 + const targetIndex = Math.max(0, index - 1);
222 + selectAndFocusTask(parent.subtasks[targetIndex]);
223 + } else {
224 + selectAndFocusTask(parent);
225 + }
226 + }
227 + }
228 +
229 + function navigateTasks(direction) {
230 + const tasks = currentTask.subtasks;
231 + const currentElement = document.activeElement;
232 + const currentContainer = currentElement.closest('.task-container');
233 + const currentIndex = Array.from(appContainer.querySelectorAll('.task-container')).indexOf(currentContainer);
234 +
235 + if (direction === 'up') {
236 + if (currentIndex > 0) {
237 + const prevTask = currentIndex === 1 ? currentTask : tasks[currentIndex - 2];
238 + selectAndFocusTask(prevTask);
239 + }
240 + } else {
241 + if (currentIndex === tasks.length) {
242 + addNewTask(currentTask);
243 + } else {
244 + selectAndFocusTask(tasks[currentIndex]);
245 + }
246 + }
247 + }
248 +
249 + function navigateInto(task) {
250 + if (task.subtasks.length > 0) {
251 + currentTask.selectedSubtaskId = task.id;
252 + taskPath.push(task);
253 + currentTask = task; // Add this line
254 + renderCurrentView();
255 + const selectedTask = task.selectedSubtaskId ?
256 + task.subtasks.find(t => t.id === task.selectedSubtaskId) :
257 + task.subtasks[0];
258 + selectAndFocusTask(selectedTask);
259 + } else {
260 + addNewTask(task);
261 + currentTask.selectedSubtaskId = task.id;
262 + taskPath.push(task);
263 + currentTask = task; // Add this line
264 + renderCurrentView();
265 + selectAndFocusTask(task.subtasks[0]);
266 + }
267 + }
268 +
269 + function navigateOut() {
270 + if (taskPath.length > 1) {
271 + const currentTaskId = currentTask.id;
272 + taskPath.pop();
273 + currentTask = taskPath[taskPath.length - 1];
274 + updateParentState(currentTask);
275 + renderCurrentView();
276 + const selectedTask = currentTask.subtasks.find(t => t.id === currentTaskId);
277 + selectAndFocusTask(selectedTask || currentTask.subtasks[0]);
278 + currentTask.selectedSubtaskId = currentTaskId;
279 + }
280 + }
281 +
282 + function reevaluateTaskState(task) {
283 + if (task.subtasks.length > 0) {
284 + const allChecked = task.subtasks.every(t => t.state === 1);
285 + const anyChecked = task.subtasks.some(t => t.state === 1);
286 +
287 + if (allChecked) {
288 + task.state = 1; // Checked
289 + } else if (!anyChecked) {
290 + task.state = 0; // Unchecked
291 + }
292 + // If some subtasks are checked and some are not, we don't change the parent's state
293 + }
294 + }
295 +
296 + function setActiveTask(input, task) {
297 + document.querySelectorAll('.active').forEach(el => el.classList.remove('active'));
298 + input.closest('.task-container').classList.add('active');
299 + if (task !== currentTask) {
300 + currentTask.selectedSubtaskId = task.id;
301 + }
302 + }
303 +
304 + function renderCurrentView() {
305 + appContainer.innerHTML = '';
306 + currentTask = taskPath[taskPath.length - 1];
307 +
308 + const parentElement = createTaskElement(currentTask, true);
309 + appContainer.appendChild(parentElement);
310 +
311 + const subtasksList = document.createElement('ul');
312 + currentTask.subtasks.forEach(subtask => {
313 + const li = document.createElement('li');
314 + li.appendChild(createTaskElement(subtask));
315 + subtasksList.appendChild(li);
316 + });
317 + appContainer.appendChild(subtasksList);
318 +
319 + const parentCheckbox = parentElement.querySelector('input[type="checkbox"]');
320 + updateCheckboxState(parentCheckbox, currentTask.state);
321 + }
322 +
323 + renderCurrentView();
324 + if (currentTask.subtasks.length > 0) {
325 + setTimeout(() => selectAndFocusTask(currentTask.subtasks[0]), 0);
326 + }
327 + });
328 + </script>
329 +</head>
330 +<body>
331 + <div id="app-container"></div>
332 +</body>
333 +</html>