render.js (6.2 KB)


  1 // Render: view rendering, breadcrumbs, UI updates, and shared DOM helpers
  2 
  3 function generateBreadcrumbs(rootTask, currentPath, selectedTaskId) {
  4 	var breadcrumbs = '';
  5 	var currentTask = rootTask;
  6 	var currentDepth = 0;
  7 
  8 	for (var i = 0; i < currentPath.length - 1; i++) {
  9 		breadcrumbs += '○ ';
 10 		currentTask = currentTask.subtasks.find(t => t.id === currentPath[i + 1].id);
 11 	}
 12 
 13 	breadcrumbs += '● ';
 14 	currentDepth = currentPath.length - 1;
 15 
 16 	if (selectedTaskId !== currentTask.id) {
 17 		var selectedTask = currentTask.subtasks.find(t => t.id === selectedTaskId);
 18 
 19 		if (selectedTask) {
 20 			function calculateMaxDepth(task, depth) {
 21 				if (task.subtasks.length === 0) return depth;
 22 				return Math.max(...task.subtasks.map(st => calculateMaxDepth(st, depth + 1)));
 23 			}
 24 
 25 			var maxDepth = calculateMaxDepth(selectedTask, currentDepth + 1);
 26 
 27 			for (var i = currentDepth + 1; i < maxDepth; i++) {
 28 				breadcrumbs += '○ ';
 29 			}
 30 		}
 31 	}
 32 
 33 	return breadcrumbs.trim();
 34 }
 35 
 36 function applyShakeAnimation(taskId, direction = 'horizontal') {
 37 	var checkbox = document.querySelector(`.task-container[data-id="${taskId}"] .checkbox-label`);
 38 	if (checkbox) {
 39 		var className = direction === 'vertical' ? 'shake-vertical' : 'shake';
 40 		if (checkbox.dataset.shaking) return;
 41 		checkbox.dataset.shaking = '1';
 42 		checkbox.classList.add(className);
 43 		checkbox.addEventListener('animationend', () => {
 44 			checkbox.classList.remove(className);
 45 		}, { once: true });
 46 		var clearShaking = () => {
 47 			delete checkbox.dataset.shaking;
 48 			document.removeEventListener('keyup', clearShaking);
 49 		};
 50 		document.addEventListener('keyup', clearShaking);
 51 	}
 52 }
 53 
 54 function selectAndFocusTask(task, cursorPos) {
 55 	var taskInput = document.querySelector(`.task-container[data-id="${task.id}"] input[type="text"]`);
 56 	if (taskInput) {
 57 		taskInput.focus();
 58 		var pos = cursorPos != null ? cursorPos : taskInput.value.length;
 59 		taskInput.setSelectionRange(pos, pos);
 60 		setActiveTask(taskInput, task);
 61 	}
 62 }
 63 
 64 function placeCursorAtBeginning(input) {
 65 	input.setSelectionRange(0, 0);
 66 }
 67 
 68 function setActiveTask(input, task) {
 69 	document.querySelectorAll('.active').forEach(el => el.classList.remove('active'));
 70 	input.closest('.task-container').classList.add('active');
 71 	if (task !== state.currentTask) {
 72 		state.currentTask.selectedSubtaskId = task.id;
 73 	}
 74 	// Re-apply multi-select highlights after clearing
 75 	if (state.multiSelectedIds.length > 1) {
 76 		applyMultiSelectHighlights();
 77 	}
 78 	updateBreadcrumbs(task);
 79 	// During multi-select, check bottommost selected task for "last subtask" status
 80 	var bottomTask = state.multiSelectedIds.length > 1
 81 		? getMultiSelectedTasks().slice(-1)[0]
 82 		: task;
 83 	state.lastSubtaskDownArrowReleased = isLastSubtask(bottomTask);
 84 	state.lastSubtaskShiftDownReleased = isLastSubtask(bottomTask);
 85 	input.focus();
 86 
 87 	// Center the active task in the viewport
 88 	var activeTaskElement = input.closest('.task-container');
 89 	if (activeTaskElement && activeTaskElement.parentElement && activeTaskElement.parentElement.tagName === 'LI') {
 90 		activeTaskElement.scrollIntoView({
 91 			behavior: 'auto',
 92 			block: 'center',
 93 			inline: 'nearest'
 94 		});
 95 
 96 		setTimeout(() => {
 97 			document.documentElement.style.scrollBehavior = 'smooth';
 98 		}, 200);
 99 	}
100 }
101 
102 function updateBreadcrumbs(selectedTask) {
103 	var breadcrumbsContainer = document.getElementById('breadcrumbs');
104 	var effectiveId = state.multiSelectedIds.length > 1 ? state.currentTask.id : selectedTask.id;
105 	var trail = generateBreadcrumbs(state.taskPath[0], state.taskPath, effectiveId);
106 	breadcrumbsContainer.textContent = trail;
107 }
108 
109 function updatePageTitle(task) {
110 	document.title = task.text || '?';
111 }
112 
113 function selectFirstSubtask() {
114 	if (state.currentTask.subtasks.length > 0) {
115 		var firstSubtask = state.currentTask.subtasks[0];
116 		selectAndFocusTask(firstSubtask);
117 	} else {
118 		selectAndFocusTask(state.currentTask);
119 	}
120 }
121 
122 function handleCopyAndCut(e) {
123 	if ((e.ctrlKey || e.metaKey) && (e.key === 'c' || e.key === 'x')) {
124 		var activeTaskInput = document.querySelector('.task-container.active input[type="text"]');
125 		if (activeTaskInput) {
126 			e.preventDefault();
127 
128 			if (activeTaskInput.selectionStart === activeTaskInput.selectionEnd) {
129 				activeTaskInput.select();
130 			}
131 
132 			if (e.key === 'c') {
133 				document.execCommand('copy');
134 			} else if (e.key === 'x') {
135 				document.execCommand('cut');
136 
137 				var taskContainer = activeTaskInput.closest('.task-container');
138 				var taskId = taskContainer.dataset.id;
139 				var task = state.currentTask.id === taskId ? state.currentTask : state.currentTask.subtasks.find(t => t.id === taskId);
140 				if (task) {
141 					task.text = activeTaskInput.value;
142 					scheduleSave();
143 				}
144 			}
145 
146 			if (e.key === 'c' && activeTaskInput.selectionStart === 0 && activeTaskInput.selectionEnd === activeTaskInput.value.length) {
147 				activeTaskInput.setSelectionRange(activeTaskInput.value.length, activeTaskInput.value.length);
148 			}
149 		}
150 	}
151 }
152 
153 function renderCurrentView() {
154 	state.appContainer.innerHTML = '';
155 	state.currentTask = state.taskPath[state.taskPath.length - 1];
156 
157 	var stickyHeader = document.createElement('div');
158 	stickyHeader.className = 'sticky-header';
159 
160 	var breadcrumbsElement = document.createElement('div');
161 	breadcrumbsElement.id = 'breadcrumbs';
162 	stickyHeader.appendChild(breadcrumbsElement);
163 
164 	var parentElement = createTaskElement(state.currentTask, true);
165 	stickyHeader.appendChild(parentElement);
166 
167 	state.appContainer.appendChild(stickyHeader);
168 
169 	var subtasksContainer = document.createElement('div');
170 	subtasksContainer.id = 'subtasks-container';
171 
172 	var subtasksList = document.createElement('ul');
173 	state.currentTask.subtasks.forEach(subtask => {
174 		var li = document.createElement('li');
175 		li.appendChild(createTaskElement(subtask));
176 		subtasksList.appendChild(li);
177 	});
178 	subtasksContainer.appendChild(subtasksList);
179 	state.appContainer.appendChild(subtasksContainer);
180 
181 	updateBreadcrumbs(state.currentTask);
182 	updatePageTitle(state.currentTask);
183 
184 	var parentCheckbox = parentElement.querySelector('input[type="checkbox"]');
185 	updateCheckboxState(parentCheckbox, state.currentTask.state);
186 
187 	if (state.currentTask.selectedSubtaskId) {
188 		var selectedTask = state.currentTask.subtasks.find(t => t.id === state.currentTask.selectedSubtaskId);
189 		if (selectedTask) {
190 			selectAndFocusTask(selectedTask);
191 		} else {
192 			selectFirstSubtask();
193 		}
194 	} else {
195 		selectFirstSubtask();
196 	}
197 }