fix event listener leak; ensure unique IDs (fix jumping bug)

Hunter
Saturday, March 7th, 2026 at 4:44 PM

index.html+37-20

index.html
 454  				return serialized;
 455  			}
 456  
 457 +			function generateId() {
 458 +				return Date.now().toString(36) + Math.random().toString(36).slice(2)
 459 +			}
 460 +
 461  			function loadTasksFromLocalStorage() {
 462  				const savedTasks = localStorage.getItem('taskTree');
 463  				if (savedTasks) {

 465  					console.log(savedTasks);
 466  					return deserializeTaskTree(savedTasks);
 467  				} else {
     -					return { id: 'root', text: 'todo', state: 0, subtasks: [{ id: Date.now(), text: '', state: 0, subtasks: [] }], selectedSubtaskId: null };
 468 +					return { id: 'root', text: 'todo', state: 0, subtasks: [{ id: generateId(), text: '', state: 0, subtasks: [] }], selectedSubtaskId: null };
 469  				}
 470  			}
 471  

 480  					const text = line.slice(depth + 2);
 481  					
 482  					const newTask = {
     -						id: depth === 0 ? 'root' : Date.now() + Math.random(),
 483 +						id: depth === 0 ? 'root' : generateId(),
 484  						text: text,
 485  						state: status === '_' ? 0 : (status === 'x' ? 1 : 2),
 486  						subtasks: [],

 631  				taskInput.addEventListener('keydown', keydownHandler);
 632  				taskInput.addEventListener('keyup', keyupHandler);
 633  				taskInput.addEventListener('keydown', handleCopyAndCut);
     -				appContainer.addEventListener('focusin', function(e) {
     -					if (e.target.tagName === 'INPUT' && e.target.type === 'text') {
     -						document.querySelectorAll('input[type="text"]').forEach(input => {
     -							if (input !== e.target) {
     -								placeCursorAtBeginning(input);
     -							}
     -						});
     -					}
     -				});
     -
 634  				taskInput.addEventListener('input', () => {
 635  					task.text = taskInput.value;
 636  					if (task === currentTask) {

1053  
1054  
1055  			function addNewSubtask(parentTask, currentSubtask = null) {
     -				const newSubtask = { id: Date.now(), text: '', state: 0, subtasks: [], selectedSubtaskId: null };
1056 +				const newSubtask = { id: generateId(), text: '', state: 0, subtasks: [], selectedSubtaskId: null };
1057  				if (currentSubtask) {
1058  					const index = parentTask.subtasks.findIndex(t => t.id === currentSubtask.id);
1059  					parentTask.subtasks.splice(index + 1, 0, newSubtask);

1083  
1084  			function navigateTasks(direction) {
1085  				const tasks = currentTask.subtasks;
1086 +				const subtaskIndex = tasks.findIndex(t => t.id === currentTask.selectedSubtaskId);
1087 +
1088 +				// Check if the parent task itself is focused
1089  				const currentElement = document.activeElement;
     -				const currentContainer = currentElement.closest('.task-container');
     -				const currentIndex = Array.from(appContainer.querySelectorAll('.task-container')).indexOf(currentContainer);
1090 +				const currentContainer = currentElement ? currentElement.closest('.task-container') : null;
1091 +				const isParentFocused = currentContainer && currentContainer.dataset.id == currentTask.id;
1092  
1093  				if (direction === 'up') {
     -					if (currentIndex > 0) {
     -						const prevTask = currentIndex === 1 ? currentTask : tasks[currentIndex - 2];
     -						selectAndFocusTask(prevTask);
1094 +					if (isParentFocused) {
1095 +						// Already at parent, can't go higher
1096 +					} else if (subtaskIndex <= 0) {
1097 +						// At first subtask or not found, go to parent
1098 +						selectAndFocusTask(currentTask);
1099 +					} else {
1100 +						selectAndFocusTask(tasks[subtaskIndex - 1]);
1101  					}
1102  				} else {
     -					if (currentIndex < tasks.length) {
     -						selectAndFocusTask(tasks[currentIndex]);
1103 +					if (isParentFocused) {
1104 +						// Move from parent to first subtask
1105 +						if (tasks.length > 0) {
1106 +							selectAndFocusTask(tasks[0]);
1107 +						}
1108 +					} else if (subtaskIndex >= 0 && subtaskIndex < tasks.length - 1) {
1109 +						selectAndFocusTask(tasks[subtaskIndex + 1]);
1110  					}
1111  				}
1112  				lastSubtaskDownArrowReleased = false;

1443  				e.preventDefault();
1444  			}
1445  
1446 +			// Register focusin handler once (not per-element) to reset cursor on non-focused inputs
1447 +			appContainer.addEventListener('focusin', function(e) {
1448 +				if (e.target.tagName === 'INPUT' && e.target.type === 'text') {
1449 +					document.querySelectorAll('input[type="text"]').forEach(input => {
1450 +						if (input !== e.target) {
1451 +							placeCursorAtBeginning(input);
1452 +						}
1453 +					});
1454 +				}
1455 +			});
1456 +
1457  			getThemesFromCSS();
1458  			setInitialTheme();
1459  			renderCurrentView();