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 }