commit 5d729515bbd565ab0b8465db01bca949e0ac200c
parent dc9ff492cf38cc7f13c4c81285d78a271917163f
Author: Hunter
Date:   Wed,  2 Jul 2025 00:23:07 -0400

⌘ + shift + arrow to (move task & navigate)

Diffstat:
Mindex.html | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mreadme.md | 24++++++++++++++----------
2 files changed, 151 insertions(+), 12 deletions(-)

diff --git a/index.html b/index.html @@ -877,6 +877,15 @@ } else if (e.key === 'ArrowDown' && !e.shiftKey) { e.preventDefault(); navigateTasks('down'); + } else if (e.key === 'ArrowLeft' && (e.metaKey || e.ctrlKey) && e.shiftKey) { + e.preventDefault(); + pullSubtaskOutLayerAndNavigate(task); + } else if (e.key === 'ArrowUp' && (e.metaKey || e.ctrlKey) && e.shiftKey) { + e.preventDefault(); + pushSubtaskIntoTargetAndNavigate(task, 'up'); + } else if (e.key === 'ArrowDown' && (e.metaKey || e.ctrlKey) && e.shiftKey) { + e.preventDefault(); + pushSubtaskIntoTargetAndNavigate(task, 'down'); } else if (e.key === 'ArrowUp' && e.shiftKey) { e.preventDefault(); moveSubtask(task, 'up'); @@ -966,6 +975,48 @@ scheduleSave(); } + function pushSubtaskIntoTargetAndNavigate(subtask, direction) { + // Don't allow pushing the current parent task + if (subtask === currentTask) { + return; + } + + // Temporarily disable smooth scrolling + document.documentElement.style.scrollBehavior = 'auto'; + + const parentTask = findParentTask(subtask); + if (!parentTask) return; + + const index = parentTask.subtasks.findIndex(t => t.id === subtask.id); + if (index === -1) return; + + let targetTask = null; + if (direction === 'up' && index > 0) { + targetTask = parentTask.subtasks[index - 1]; + } else if (direction === 'down' && index < parentTask.subtasks.length - 1) { + targetTask = parentTask.subtasks[index + 1]; + } + + if (!targetTask) return; + + // Remove the subtask from its current parent + parentTask.subtasks.splice(index, 1); + + // Add the subtask to the target task's subtasks + targetTask.subtasks.push(subtask); + + // Update parent task state after removal + updateTaskAndAncestors(parentTask); + + // Update target task state after addition + updateTaskAndAncestors(targetTask); + + // Navigate into the target task and select the moved subtask + navigateIntoTaskAndSelectSubtask(targetTask, subtask); + + scheduleSave(); + } + function pullSubtaskOutLayer(subtask) { // Don't allow pulling the current parent task if (subtask === currentTask) { @@ -1005,8 +1056,8 @@ // Handle view and selection based on remaining tasks at current level if (currentParent.subtasks.length === 0 && taskPath.length > 1) { - // No remaining subtasks, navigate to parent level - navigateToParentTask(); + // No remaining subtasks, navigate to parent level and select the moved task + navigateToParentTaskAndSelectTask(subtask); } else { // Stay at current level and select adjacent task renderCurrentView(); @@ -1023,6 +1074,52 @@ scheduleSave(); } + function pullSubtaskOutLayerAndNavigate(subtask) { + // Don't allow pulling the current parent task + if (subtask === currentTask) { + return; + } + + // Don't allow pulling when at root level + if (taskPath.length <= 1) { + return; + } + + // Temporarily disable smooth scrolling + document.documentElement.style.scrollBehavior = 'auto'; + + const currentParent = findParentTask(subtask); + if (!currentParent) return; + + // Find the grandparent (the outer layer we're pulling into) + const grandParent = findGrandParentTask(currentParent); + if (!grandParent) return; + + // Remove the subtask from its current parent + const currentIndex = currentParent.subtasks.findIndex(t => t.id === subtask.id); + if (currentIndex === -1) return; + currentParent.subtasks.splice(currentIndex, 1); + + // Find where to insert in the grandparent's subtasks + const currentParentIndex = grandParent.subtasks.findIndex(t => t.id === currentParent.id); + if (currentParentIndex === -1) { + // Fallback: add to end of grandparent's subtasks + grandParent.subtasks.push(subtask); + } else { + // Insert after the current parent's position + grandParent.subtasks.splice(currentParentIndex + 1, 0, subtask); + } + + // Update task states + updateTaskAndAncestors(currentParent); + updateTaskAndAncestors(grandParent); + + // Always navigate to parent level and select the moved task + navigateToParentTaskAndSelectTask(subtask); + + scheduleSave(); + } + function findGrandParentTask(parentTask) { // Find the grandparent of the given parent task for (let i = taskPath.length - 1; i >= 0; i--) { @@ -1107,6 +1204,24 @@ } } + function navigateIntoTaskAndSelectSubtask(targetTask, subtaskToSelect) { + // Temporarily disable smooth scrolling + document.documentElement.style.scrollBehavior = 'auto'; + + // Set the selectedSubtaskId on the current task before navigating + currentTask.selectedSubtaskId = targetTask.id; + + // Navigate into the target task + taskPath.push(targetTask); + currentTask = targetTask; + updateBreadcrumbs(currentTask); + renderCurrentView(); + + // Select the moved subtask + selectAndFocusTask(subtaskToSelect); + currentTask.selectedSubtaskId = subtaskToSelect.id; + } + function navigateToParentTask() { // Check if the current active parent task is the root if (currentTask.id === 'root') { @@ -1138,6 +1253,26 @@ } } + function navigateToParentTaskAndSelectTask(targetTask) { + // Check if the current active parent task is the root + if (currentTask.id === 'root') { + // We're at root level, just re-render and select the target task + renderCurrentView(); + selectAndFocusTask(targetTask); + } else if (taskPath.length > 1) { + // Normal navigation out - we're deeper than root level + // Temporarily disable smooth scrolling + document.documentElement.style.scrollBehavior = 'auto'; + + taskPath.pop(); + currentTask = taskPath[taskPath.length - 1]; + updateBreadcrumbs(currentTask); + renderCurrentView(); + selectAndFocusTask(targetTask); + currentTask.selectedSubtaskId = targetTask.id; + } + } + function setActiveTask(input, task) { document.querySelectorAll('.active').forEach(el => el.classList.remove('active')); input.closest('.task-container').classList.add('active'); diff --git a/readme.md b/readme.md @@ -16,28 +16,32 @@ the nested todo list that breaks complex tasks into manageable subtasks. ### quickstart 1. press the `Return` / `Enter` key to add subtasks to the root "todo" task 2. give each new subtask a meaningful name -3. use `Shift + Arrow Right` to navigate into a subtask -4. use `Shift + Arrow Left` to return to the parent task +3. use `Shift + ➡️` to navigate into a subtask +4. use `Shift + ⬅️` to return to the parent task 5. use `Shift + Enter` to quickly mark tasks as complete or incomplete ## controls ### navigation -- `Arrow Up/Down` move between tasks at the same level -- `Shift + Arrow Right` navigate into a subtask -- `Shift + Arrow Left` return to the enclosing parent task +- `⬆️/⬇️` move between tasks at the same level +- `Shift + ➡️` navigate into a subtask +- `Shift + ⬅️` return to the enclosing parent task ### task management - `Enter` add a new task -- `Arrow Down` (on last subtask) add a new task at the bottom of the list +- `⬇️` (on last subtask) add a new task at the bottom of the list - `Backspace` (when selected task's text is empty) remove the task and its subtasks - `Shift + Enter` toggle selected task's completion status -- `Shift + Arrow Up/Down` reposition the selected task within its current level -- `⌘ + Arrow Up/Down` move the selected task into the task above/below it -- `⌘ + Arrow Left` pull the selected task out one level (to its parent's level) +- `Shift + ⬆️/⬇️` reposition the selected task within its current level +- `⌘ + ⬆️/⬇️` push the selected task into the task above or below it +- `⌘ + ⬅️` pull the selected task out one level (to the level of its parent) + +### combined movement + navigation +- `⌘ + Shift + ⬆️/⬇️` push the selected task into the task above or below it AND navigate to its new position +- `⌘ + Shift + ⬅️` pull the selected task out one level AND navigate to its new position ### text editing -- `Left/Right` move text cursor within selected task +- `⬅️/➡️` move text cursor within selected task - `⌘ + C` copy the selected task's text (or highlighted substring) - `⌘ + X` cut the selected task's text (or highlighted substring) - `⌘ + V` paste text content from the clipboard