commit 3b5400b5de86da03fe35e9023e27c90b063d7cda
parent eb657dfabbe7d9bb832910017b14832095f269f8
Author: Hunter Irving
Date:   Wed,  2 Jul 2025 00:27:10 -0400

Merge pull request #3 from hunterirving/pull-in-pull-out

Push and Pull Tasks Between Layers
Diffstat:
Mindex.html | 254+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mreadme.md | 36+++++++++++++++++++++---------------
2 files changed, 275 insertions(+), 15 deletions(-)

diff --git a/index.html b/index.html @@ -865,12 +865,27 @@ keyHandler.shiftEnter.pressed = true; toggleTaskState(task); } + } else if (e.key === 'ArrowUp' && (e.metaKey || e.ctrlKey) && !e.shiftKey) { + e.preventDefault(); + pushSubtaskIntoTarget(task, 'up'); + } else if (e.key === 'ArrowDown' && (e.metaKey || e.ctrlKey) && !e.shiftKey) { + e.preventDefault(); + pushSubtaskIntoTarget(task, 'down'); } else if (e.key === 'ArrowUp' && !e.shiftKey) { e.preventDefault(); navigateTasks('up'); } 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'); @@ -888,6 +903,9 @@ applyShakeAnimation(task.id); } } + } else if (e.key === 'ArrowLeft' && (e.metaKey || e.ctrlKey) && !e.shiftKey) { + e.preventDefault(); + pullSubtaskOutLayer(task); } else if (e.key === 'ArrowLeft' && e.shiftKey) { e.preventDefault(); if (!keyHandler.shiftLeft.pressed) { @@ -915,6 +933,204 @@ scheduleSave(); } + function pushSubtaskIntoTarget(subtask, direction) { + // Don't allow pushing the current parent task + if (subtask === currentTask) { + return; + } + + 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); + + // Re-render the view + renderCurrentView(); + + // Focus on the target task that now contains the moved subtask + selectAndFocusTask(targetTask); + + 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) { + return; + } + + // Don't allow pulling when at root level + if (taskPath.length <= 1) { + return; + } + + 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); + + // 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 and select the moved task + navigateToParentTaskAndSelectTask(subtask); + } else { + // Stay at current level and select adjacent task + renderCurrentView(); + if (currentParent.subtasks.length > 0) { + // Select the task that was above the moved one, or below if at the top + const targetIndex = Math.max(0, currentIndex - 1); + selectAndFocusTask(currentParent.subtasks[targetIndex]); + } else { + // Focus on the parent task if no subtasks remain + selectAndFocusTask(currentParent); + } + } + + 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--) { + const potentialGrandParent = taskPath[i]; + if (potentialGrandParent.subtasks.some(t => t.id === parentTask.id)) { + return potentialGrandParent; + } + } + return null; + } + function addNewSubtask(parentTask, currentSubtask = null) { const newSubtask = { id: Date.now(), text: '', state: 0, subtasks: [], selectedSubtaskId: null }; if (currentSubtask) { @@ -988,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') { @@ -1019,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,31 +16,37 @@ 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 +- `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 -- `Command/Ctrl + C` copy the selected task's text (or highlighted substring) -- `Command/Ctrl + X` cut the selected task's text (or highlighted substring) -- `Command/Ctrl + V` paste text content from the clipboard -- `Command/Ctrl + Z` undo text edit -- `Command/Ctrl + Shift + Z` redo text edit +- `⬅️/➡️` 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 +- `⌘ + Z` undo text edit +- `⌘ + Shift + Z` redo text edit ## data persistence your task tree is automatically saved to your browser's local storage after each edit. this ensures your tasks will persist even if you close the browser or refresh the page. @@ -48,8 +54,8 @@ your task tree is automatically saved to your browser's local storage after each > [!WARNING] > clearing your browsing data may erase your saved tasks. to avoid losing progress, create a manual backup before clearing your browsing data. -- `Command/Ctrl + S` export tasks to .txt file -- `Command/Ctrl + O` import tasks from .txt file +- `⌘ + S` export tasks to .txt file +- `⌘ + O` import tasks from .txt file ## themes press `F2` to cycle through available themes.