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 }