task-element.js (9.1 KB)
1 // Task element: DOM creation and per-element event handlers 2 3 function createTaskElement(task, isParentTask = false) { 4 var taskContainer = document.createElement('div'); 5 taskContainer.className = 'task-container'; 6 taskContainer.dataset.id = task.id; 7 if (isParentTask) taskContainer.classList.add('parent-task'); 8 9 var checkbox = document.createElement('input'); 10 checkbox.type = 'checkbox'; 11 checkbox.className = 'custom-checkbox'; 12 checkbox.id = `checkbox-${task.id}`; 13 updateCheckboxState(checkbox, task.state); 14 checkbox.addEventListener('click', (e) => { 15 e.preventDefault(); 16 toggleTaskState(task); 17 }); 18 19 var checkboxLabel = document.createElement('label'); 20 checkboxLabel.className = 'checkbox-label'; 21 checkboxLabel.setAttribute('for', `checkbox-${task.id}`); 22 23 var taskInput = document.createElement('input'); 24 taskInput.type = 'text'; 25 taskInput.value = task.text; 26 taskInput.setAttribute('autocomplete', 'off'); 27 taskInput.setAttribute('spellcheck', 'false'); 28 taskInput.setAttribute('autocorrect', 'off'); 29 taskInput.setAttribute('autocapitalize', 'off'); 30 31 taskInput.addEventListener('mousedown', function(e) { 32 e.stopPropagation(); 33 }); 34 35 var keydownHandler = function(e) { 36 if (e.key === 'Backspace') { 37 if (keyHandler.backspace.blocked) { 38 e.preventDefault(); 39 return; 40 } 41 // Multi-select backspace handling 42 if (state.multiSelectedIds.length > 1) { 43 var selected = getMultiSelectedTasks(); 44 var allEmpty = selected.every(t => t.text === ''); 45 if (allEmpty) { 46 e.preventDefault(); 47 if (keyHandler.backspace.canDelete) { 48 keyHandler.backspace.blocked = true; 49 var toDelete = selected.filter(t => t !== state.taskPath[0] && t !== state.currentTask); 50 if (toDelete.length > 0 && !(state.currentTask.id === 'root' && toDelete.length >= state.currentTask.subtasks.length)) { 51 var firstDeleteIdx = Math.min(...toDelete.map(t => state.currentTask.subtasks.findIndex(s => s.id === t.id)).filter(i => i !== -1)); 52 for (var t of toDelete) { 53 var idx = state.currentTask.subtasks.findIndex(s => s.id === t.id); 54 if (idx !== -1) state.currentTask.subtasks.splice(idx, 1); 55 } 56 clearMultiSelect(); 57 updateTaskAndAncestors(state.currentTask); 58 if (state.currentTask.subtasks.length === 0 && state.taskPath.length > 1) { 59 navigateToParentTask(); 60 } else { 61 renderCurrentView(); 62 var targetIndex = Math.max(0, firstDeleteIdx - 1); 63 selectAndFocusTask(state.currentTask.subtasks[targetIndex]); 64 } 65 scheduleSave(); 66 } else { 67 for (var t of selected) { 68 applyShakeAnimation(t.id); 69 } 70 } 71 } 72 return; 73 } 74 // Some tasks still have text: remove last char from all that have text 75 e.preventDefault(); 76 for (var t of selected) { 77 if (t.text.length > 0) { 78 t.text = t.text.slice(0, -1); 79 var inp = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`); 80 if (inp) inp.value = t.text; 81 } 82 } 83 keyHandler.backspace.canDelete = false; 84 scheduleSave(); 85 return; 86 } 87 if (taskInput.value === '' && keyHandler.backspace.canDelete && state.multiSelectedIds.length <= 1) { 88 e.preventDefault(); 89 if (task !== state.taskPath[0]) { 90 keyHandler.backspace.blocked = true; 91 clearMultiSelect(); 92 if (task === state.currentTask) { 93 deleteCurrentParentTask(); 94 } else { 95 deleteSubtask(task); 96 } 97 } else { 98 applyShakeAnimation(task.id); 99 } 100 } else if (taskInput.value !== '') { 101 keyHandler.backspace.canDelete = false; 102 } 103 } else if (e.key === 'Enter' && !e.shiftKey) { 104 if (keyHandler.enter.blocked) { 105 e.preventDefault(); 106 return; 107 } 108 if (keyHandler.enter.canAdd) { 109 e.preventDefault(); 110 keyHandler.enter.blocked = true; 111 clearMultiSelect(); 112 addNewSubtask(state.currentTask, task); 113 } 114 } else if (e.key === 'ArrowDown' && !e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) { 115 if (keyHandler.arrowDown.blocked) { 116 e.preventDefault(); 117 return; 118 } 119 if (isLastSubtask(task) && state.lastSubtaskDownArrowReleased && task !== state.currentTask) { 120 e.preventDefault(); 121 keyHandler.arrowDown.blocked = true; 122 addNewSubtask(state.currentTask, task); 123 state.lastSubtaskDownArrowReleased = false; 124 } else { 125 handleKeyDown(e, task); 126 } 127 } else if (e.key === 'ArrowDown' && e.shiftKey && !e.metaKey && !e.ctrlKey && !e.altKey) { 128 if (keyHandler.shiftArrowDown.blocked) { 129 e.preventDefault(); 130 return; 131 } 132 // Multi-select: insert new task above chunk when at bottom 133 if (state.multiSelectedIds.length > 1 && !e.repeat) { 134 var selected = getMultiSelectedTasks(); 135 var lastSelected = selected[selected.length - 1]; 136 if (isLastSubtask(lastSelected) && state.lastSubtaskShiftDownReleased) { 137 e.preventDefault(); 138 keyHandler.shiftArrowDown.blocked = true; 139 var topIndex = state.currentTask.subtasks.findIndex(t => state.multiSelectedIds.includes(t.id)); 140 var newSubtask = { id: generateId(), text: '', state: 0, subtasks: [], selectedSubtaskId: null }; 141 state.currentTask.subtasks.splice(topIndex, 0, newSubtask); 142 updateTaskAndAncestors(state.currentTask); 143 state.currentTask.selectedSubtaskId = state.multiSelectAnchorId; 144 renderCurrentView(); 145 applyMultiSelectHighlights(); 146 scheduleSave(); 147 state.lastSubtaskShiftDownReleased = false; 148 } else { 149 handleKeyDown(e, task); 150 } 151 } else if (isLastSubtask(task) && state.lastSubtaskShiftDownReleased && task !== state.currentTask && !e.repeat) { 152 e.preventDefault(); 153 keyHandler.shiftArrowDown.blocked = true; 154 var parentTask = findParentTask(task); 155 if (parentTask) { 156 var index = parentTask.subtasks.findIndex(t => t.id === task.id); 157 var newSubtask = { id: generateId(), text: '', state: 0, subtasks: [], selectedSubtaskId: null }; 158 parentTask.subtasks.splice(index, 0, newSubtask); 159 updateTaskAndAncestors(parentTask); 160 renderCurrentView(); 161 selectAndFocusTask(task); 162 scheduleSave(); 163 } 164 state.lastSubtaskShiftDownReleased = false; 165 } else { 166 handleKeyDown(e, task); 167 } 168 } else { 169 handleKeyDown(e, task); 170 } 171 }; 172 173 var keyupHandler = function(e) { 174 if (e.key === 'Backspace') { 175 keyHandler.backspace.canDelete = true; 176 keyHandler.backspace.blocked = false; 177 } else if (e.key === 'Enter') { 178 keyHandler.enter.canAdd = true; 179 keyHandler.enter.blocked = false; 180 keyHandler.shiftEnter.pressed = false; 181 } else if (e.key === 'ArrowDown') { 182 keyHandler.arrowDown.canAdd = true; 183 keyHandler.arrowDown.blocked = false; 184 keyHandler.shiftArrowDown.blocked = false; 185 var isAtBottom = state.multiSelectedIds.length > 1 186 ? isLastSubtask(getMultiSelectedTasks().slice(-1)[0]) 187 : isLastSubtask(task); 188 if (isAtBottom) { 189 state.lastSubtaskDownArrowReleased = true; 190 state.lastSubtaskShiftDownReleased = true; 191 } else { 192 state.lastSubtaskDownArrowReleased = false; 193 state.lastSubtaskShiftDownReleased = false; 194 } 195 } else if (e.key === 'ArrowRight') { 196 keyHandler.shiftRight.pressed = false; 197 } else if (e.key === 'ArrowLeft') { 198 keyHandler.shiftLeft.pressed = false; 199 } 200 }; 201 202 taskInput.addEventListener('keydown', keydownHandler); 203 taskInput.addEventListener('keyup', keyupHandler); 204 taskInput.addEventListener('keydown', handleCopyAndCut); 205 taskInput.addEventListener('input', (e) => { 206 var oldText = task.text; 207 task.text = taskInput.value; 208 // Propagate edits to all other multi-selected tasks 209 if (state.multiSelectedIds.length > 1 && state.multiSelectedIds.includes(task.id)) { 210 var otherSelected = getMultiSelectedTasks().filter(t => t.id !== task.id); 211 212 if (e.inputType === 'insertText' && e.data) { 213 for (var t of otherSelected) { 214 t.text = t.text + e.data; 215 var otherInput = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`); 216 if (otherInput) otherInput.value = t.text; 217 } 218 } else if (e.inputType === 'deleteContentBackward' || e.inputType === 'deleteContentForward') { 219 for (var t of otherSelected) { 220 if (t.text.length > 0) { 221 t.text = t.text.slice(0, -1); 222 } 223 var otherInput = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`); 224 if (otherInput) otherInput.value = t.text; 225 } 226 } else if (e.inputType === 'insertFromPaste' || e.inputType === 'insertFromDrop') { 227 var addedLen = taskInput.value.length - oldText.length; 228 if (addedLen > 0) { 229 var pastedText = taskInput.value.slice(taskInput.selectionStart - addedLen, taskInput.selectionStart); 230 for (var t of otherSelected) { 231 t.text = t.text + pastedText; 232 var otherInput = document.querySelector(`.task-container[data-id="${t.id}"] input[type="text"]`); 233 if (otherInput) otherInput.value = t.text; 234 } 235 } 236 } 237 } 238 if (task === state.currentTask) { 239 updatePageTitle(task); 240 } 241 scheduleSave(); 242 }); 243 244 taskInput.addEventListener('focus', () => { 245 if (state.multiSelectedIds.length > 1 && !state.multiSelectedIds.includes(task.id)) { 246 clearMultiSelect(); 247 } 248 setActiveTask(taskInput, task); 249 }); 250 251 taskContainer.appendChild(checkbox); 252 taskContainer.appendChild(checkboxLabel); 253 taskContainer.appendChild(taskInput); 254 255 return taskContainer; 256 }