Compare commits

..

16 Commits

Author SHA1 Message Date
Yuyao Huang
01ae9c964a fix: add position: relative to scroll-view for correct offsetTop
Without position: relative, .task-item offsetParent is the body element,
causing offsetTop to be measured from document root rather than the
scroll container. This makes scrollToTask calculate wrong scrollTop.
2026-05-09 16:06:06 +08:00
Yuyao Huang
5294446407 fix: use requestAnimationFrame to defer in-focus and handler binding
requestAnimationFrame waits until after the browser has rendered the
current frame, which includes processing the async scroll event queued
by scrollToTask. This ensures in-focus is set after the scroll event
fires, not overwritten by it.
2026-05-09 16:04:08 +08:00
Yuyao Huang
ab000bcd41 fix: delay scroll handler binding to skip queued scroll event
scrollTop assignment triggers an async scroll event. When all tasks
fit in the viewport, handleScrollFocus recalculates center-aligned
task and picks the last one instead of the saved one. Using setTimeout(0)
defers handler binding to after the queued event fires.
2026-05-09 15:59:13 +08:00
Yuyao Huang
2229fdd0ef fix: directly set in-focus on scrolled task instead of recalculating geometry
Previously handleScrollFocus() recalculated the centered task during
init, which could select a different task than the one scrollToTask()
targeted due to scroll container clamping or DOM layout timing.

Now the scrolled task directly receives the in-focus class, and
handleScrollFocus is only used during user-initiated scroll events.
Also removes the isInitializing flag as it's no longer needed.
2026-05-09 15:52:24 +08:00
Yuyao Huang
1a23558cad fix: restore center alignment in scrollToTask to match handleScrollFocus
Both scrollToTask and handleScrollFocus now use center-of-viewport
calculation, ensuring consistent behavior between scrolling and
in-focus detection.
2026-05-09 15:44:47 +08:00
Yuyao Huang
84181e1ec2 fix: use scrollTop instead of viewport center for in-focus detection
handleScrollFocus now finds the task closest to scrollTop (offset)
instead of closest to the vertical center of the viewport. This
ensures that the saved task matches the user's scroll target.
2026-05-09 15:37:34 +08:00
Yuyao Huang
eca0cf4193 fix: call handleScrollFocus before binding scroll event handlers
Call handleScrollFocus before adding scroll event listeners to prevent
handleScrollSave from triggering during initial setup.
2026-05-09 15:33:40 +08:00
Yuyao Huang
14ebbda585 fix: skip save during initial handleScrollFocus call
Wrap the initial handleScrollFocus call with isInitializing flag
to prevent handleScrollSave from incorrectly updating selected_task_id
2026-05-09 15:29:01 +08:00
Yuyao Huang
fd92c6fe96 fix: save the in-focus task instead of recalculating top task
handleScrollSave now saves the task with in-focus class (determined by
handleScrollFocus as the centered task) rather than recalculating which
task is closest to the top. This ensures consistency between what's
highlighted and what's saved.
2026-05-09 15:21:13 +08:00
Yuyao Huang
12610d26c0 Add debugging logs to trace scroll save behavior 2026-05-09 15:14:11 +08:00
Yuyao Huang
1df90490e6 fix: restore scroll position and in-focus highlighting correctly
- Set selectedTaskId when loading saved task
- Call handleScrollFocus initially to set in-focus class
- Skip saving during initialization unless task matches saved
2026-05-09 14:51:41 +08:00
Yuyao Huang
ca7bd7e24e fix: align task to top instead of center in scroll view 2026-05-09 14:47:03 +08:00
Yuyao Huang
025195be27 Add detailed scroll position diagnostics 2026-05-09 14:43:29 +08:00
Yuyao Huang
0f1fa712a9 Add delayed scrollTop checks to catch post-load changes 2026-05-09 14:39:00 +08:00
Yuyao Huang
df74f1b8a7 Add end-of-loadTasks scrollTop debug log 2026-05-09 14:35:28 +08:00
Yuyao Huang
295fde8a75 Add debug logging for goals data 2026-05-09 14:26:57 +08:00
2 changed files with 33 additions and 22 deletions

View File

@ -44,6 +44,7 @@
flex: 1; flex: 1;
height: 600px; height: 600px;
overflow-y: auto; overflow-y: auto;
position: relative;
background: white; background: white;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 4px rgba(0,0,0,0.1);

View File

@ -5,7 +5,6 @@ let selectedTaskId = null;
let sortableInstance = null; let sortableInstance = null;
let persistTimer = null; let persistTimer = null;
let currentUser = null; let currentUser = null;
let isInitializing = false;
async function loadGoals() { async function loadGoals() {
try { try {
@ -38,31 +37,29 @@ async function loadGoals() {
async function loadTasks() { async function loadTasks() {
if (!selectedGoalId) return; if (!selectedGoalId) return;
isInitializing = true;
try { try {
tasks = await get(`/api/tasks?goal_id=${selectedGoalId}`); tasks = await get(`/api/tasks?goal_id=${selectedGoalId}`);
console.log("loadTasks: got", tasks.length, "tasks");
console.log("Task order:", tasks.map(t => ({id: t.id, title: t.title, status: t.status})));
renderTasks(); renderTasks();
initSortable(); initSortable();
const currentGoal = goals.find(g => g.id === selectedGoalId); const currentGoal = goals.find(g => g.id === selectedGoalId);
const savedTaskId = currentGoal ? currentGoal.selected_task_id : null; const savedTaskId = currentGoal ? currentGoal.selected_task_id : null;
console.log("loadTasks: savedTaskId from goal =", savedTaskId);
const savedTaskExists = savedTaskId && tasks.some(t => t.id === savedTaskId); const savedTaskExists = savedTaskId && tasks.some(t => t.id === savedTaskId);
console.log("loadTasks: savedTaskExists =", savedTaskExists);
let focusTaskId = null;
if (savedTaskExists) { if (savedTaskExists) {
console.log("loadTasks: using savedTaskId path"); focusTaskId = savedTaskId;
scrollToTask(savedTaskId); scrollToTask(savedTaskId);
selectedTaskId = savedTaskId;
if (isLandscapeMode()) { if (isLandscapeMode()) {
selectTask(savedTaskId); selectTask(savedTaskId);
} }
} else { } else {
console.log("loadTasks: using fallback path");
const doingTask = tasks.find(t => t.status === "doing"); const doingTask = tasks.find(t => t.status === "doing");
if (doingTask) { if (doingTask) {
focusTaskId = doingTask.id;
scrollToTask(doingTask.id); scrollToTask(doingTask.id);
if (isLandscapeMode()) { if (isLandscapeMode()) {
selectTask(doingTask.id); selectTask(doingTask.id);
@ -72,22 +69,32 @@ async function loadTasks() {
} }
} }
// Wait one frame so the async scroll event from scrollToTask fires first,
// then set in-focus and bind handlers after it has been consumed.
requestAnimationFrame(() => {
if (focusTaskId) {
document.querySelectorAll(".task-item.in-focus").forEach(el => el.classList.remove("in-focus"));
const focusEl = document.querySelector(`[data-task-id="${focusTaskId}"]`);
if (focusEl) {
focusEl.classList.add("in-focus");
}
}
initScrollFocus(); initScrollFocus();
});
} catch (error) { } catch (error) {
console.error("Failed to load tasks:", error); console.error("Failed to load tasks:", error);
} finally {
isInitializing = false;
} }
} }
async function persistSelectedTask(taskId) { async function persistSelectedTask(taskId) {
if (!selectedGoalId || isInitializing) return; if (!selectedGoalId) return;
try { try {
await patch(`/api/goals/${selectedGoalId}/selected-task`, { task_id: taskId }); await patch(`/api/goals/${selectedGoalId}/selected-task`, { task_id: taskId });
const goal = goals.find(g => g.id === selectedGoalId); const goal = goals.find(g => g.id === selectedGoalId);
if (goal) { if (goal) {
goal.selected_task_id = taskId; goal.selected_task_id = taskId;
} }
console.log("Saved selected task:", taskId);
} catch (error) { } catch (error) {
console.error("Failed to persist selected task:", error); console.error("Failed to persist selected task:", error);
} }
@ -150,19 +157,13 @@ function initSortable() {
} }
function scrollToTask(taskId) { function scrollToTask(taskId) {
console.log("scrollToTask called:", taskId);
const taskElement = document.querySelector(`[data-task-id="${taskId}"]`); const taskElement = document.querySelector(`[data-task-id="${taskId}"]`);
if (taskElement) { if (taskElement) {
const scrollView = document.getElementById("scroll-view"); const scrollView = document.getElementById("scroll-view");
const taskTop = taskElement.offsetTop; const taskTop = taskElement.offsetTop;
const scrollViewHeight = scrollView.clientHeight; const scrollViewHeight = scrollView.clientHeight;
const taskHeight = taskElement.offsetHeight; const taskHeight = taskElement.offsetHeight;
const targetScrollTop = taskTop - (scrollViewHeight / 2) + (taskHeight / 2); scrollView.scrollTop = taskTop - (scrollViewHeight / 2) + (taskHeight / 2);
console.log("Scrolling to:", targetScrollTop, "element offsetTop:", taskTop);
scrollView.scrollTop = targetScrollTop;
console.log("scrollTop after set:", scrollView.scrollTop);
} else {
console.log("Task element not found for id:", taskId);
} }
} }
@ -170,7 +171,9 @@ function initScrollFocus() {
const scrollView = document.getElementById("scroll-view"); const scrollView = document.getElementById("scroll-view");
scrollView.removeEventListener("scroll", handleScrollFocus); scrollView.removeEventListener("scroll", handleScrollFocus);
scrollView.removeEventListener("scroll", handleScrollSave);
scrollView.addEventListener("scroll", handleScrollFocus); scrollView.addEventListener("scroll", handleScrollFocus);
scrollView.addEventListener("scroll", handleScrollSave);
} }
function handleScrollFocus() { function handleScrollFocus() {
@ -201,9 +204,6 @@ function handleScrollFocus() {
const taskId = parseInt(closestItem.dataset.taskId); const taskId = parseInt(closestItem.dataset.taskId);
clearTimeout(persistTimer);
persistTimer = setTimeout(() => persistSelectedTask(taskId), 400);
if (isLandscapeMode()) { if (isLandscapeMode()) {
if (taskId !== selectedTaskId) { if (taskId !== selectedTaskId) {
selectTask(taskId); selectTask(taskId);
@ -212,6 +212,16 @@ function handleScrollFocus() {
} }
} }
function handleScrollSave() {
const inFocusTask = document.querySelector(".task-item.in-focus");
if (inFocusTask) {
const taskId = parseInt(inFocusTask.dataset.taskId);
clearTimeout(persistTimer);
persistTimer = setTimeout(() => persistSelectedTask(taskId), 400);
}
}
function isLandscapeMode() { function isLandscapeMode() {
return window.innerWidth > window.innerHeight && window.innerWidth >= 1024; return window.innerWidth > window.innerHeight && window.innerWidth >= 1024;
} }