todo list section collapse

This commit is contained in:
Guy Sandler 2026-10-29 17:45:26 -07:00
parent 1533b7af61
commit cb2867bbb0
5 changed files with 102 additions and 5 deletions

View File

@ -1877,6 +1877,9 @@
"todo_separate_scrollbar": { "todo_separate_scrollbar": {
"message": "Separate Scrollbar" "message": "Separate Scrollbar"
}, },
"todo_sections_persist": {
"message": "Remember collapsed sections"
},
"todo_show_scores": { "todo_show_scores": {
"message": "Show assignment scores" "message": "Show assignment scores"
}, },

View File

@ -103,6 +103,15 @@
#canvasrefined-todo-list, #canvasrefined-announcement-list {font-size: 12px} #canvasrefined-todo-list, #canvasrefined-announcement-list {font-size: 12px}
.canvasrefined-todo-container, .canvasrefined-todo-container:hover {color:#777} .canvasrefined-todo-container, .canvasrefined-todo-container:hover {color:#777}
.canvasrefined-todo-container p {margin: 0!important;} .canvasrefined-todo-container p {margin: 0!important;}
/* Better Todo: collapsible due-date sections (chevron + live item count) */
.better-todo-section-header {display:flex;align-items:center;gap:6px;font-size:12px;color:var(--bctext-0);cursor:pointer;user-select:none;}
.better-todo-section-header:hover {color:var(--bclinks);}
.better-todo-section-header .better-todo-chevron {flex:none;transform:rotate(90deg);transition:transform .2s ease;}
.better-todo-dueheader.better-todo-collapsed .better-todo-section-header .better-todo-chevron {transform:rotate(0deg);}
.better-todo-dueheader.better-todo-collapsed .todo-group-list {display:none!important;}
/* count badge: theme "Background Alt" surface with the theme's Text Main number;
in light mode (no --bc vars) it falls back to the dark-mode palette pill */
.better-todo-section-count {flex:none;margin-left:auto;background:var(--bcbackground-1,#161616);color:var(--bctext-0,#f5f5f5);border-radius:8px;padding:1px 7px;font-size:10.5px;font-weight:700;}
.canvasrefined-todo-icon svg {height: 18px; width: 18px;margin:0} .canvasrefined-todo-icon svg {height: 18px; width: 18px;margin:0}
.canvasrefined-todoitem-course {overflow: hidden; text-overflow: ellipsis;white-space: nowrap;} .canvasrefined-todoitem-course {overflow: hidden; text-overflow: ellipsis;white-space: nowrap;}
.canvasrefined-todoitem-title {font-size: 14px;font-weight:bold;display:inline;margin-top:2px;} .canvasrefined-todoitem-title {font-size: 14px;font-weight:bold;display:inline;margin-top:2px;}

View File

@ -333,6 +333,10 @@
<input type="checkbox" id="todo_remove_icons" name="todo_remove_icons"> <input type="checkbox" id="todo_remove_icons" name="todo_remove_icons">
<label for="todo_remove_icons" class="sub-text" data-i18n="todo_remove_icons">Remove icons</label> <label for="todo_remove_icons" class="sub-text" data-i18n="todo_remove_icons">Remove icons</label>
</div> </div>
<div class="sub-option">
<input type="checkbox" id="todo_sections_persist" name="todo_sections_persist">
<label for="todo_sections_persist" class="sub-text" data-i18n="todo_sections_persist">Remember collapsed sections</label>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1288,6 +1288,13 @@ function applyOptionsChanges(changes) {
case "custom_cards_3": case "custom_cards_3":
customizeCards(); customizeCards();
break; break;
case "todo_sections_persist":
// Persistence is opt-in: the moment it turns on, adopt the session's collapses;
// turned off, stored state stays put but is simply not read on the next load.
if (options.todo_sections_persist === true) {
chrome.storage.sync.set({ "todo_collapsed_sections": { ...todoCollapsedSections } });
}
break;
case "todo_hr24": case "todo_hr24":
case "todo_separate_scrollbar": case "todo_separate_scrollbar":
case "num_todo_items": case "num_todo_items":
@ -3818,10 +3825,20 @@ async function createTodoSections(location) {
else if (key >= 7 && key < 30) label = replaceCountPlaceholder(getMsg("due_weeks", "Due <strong>{{COUNT}} weeks</strong>"), Math.floor(key / 7)); else if (key >= 7 && key < 30) label = replaceCountPlaceholder(getMsg("due_weeks", "Due <strong>{{COUNT}} weeks</strong>"), Math.floor(key / 7));
else if (key == "30") label = getMsg("due_one_month", "Due <strong>1 month</strong>"); else if (key == "30") label = getMsg("due_one_month", "Due <strong>1 month</strong>");
else label = "<strong>" + dueGroupLabel(key) + "</strong>"; else label = "<strong>" + dueGroupLabel(key) + "</strong>";
makeElement("div", wrapper, { // Clickable section header: chevron + label + live item count (user suggestion:
innerHTML: "<span>" + label + "</span>", // collapsible due-date sections with a per-section item indicator).
style: "display:flex;flex-direction:column;gap:10px;font-size:12px;color:var(--bctext-0);" const sectionHeader = makeElement("div", wrapper, { "className": "better-todo-section-header" });
}) sectionHeader.setAttribute("role", "button");
sectionHeader.setAttribute("tabindex", "0");
sectionHeader.setAttribute("aria-expanded", "true");
sectionHeader.innerHTML =
'<svg class="better-todo-chevron" viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg>' +
'<span class="better-todo-section-label">' + label + '</span>' +
'<span class="better-todo-section-count" aria-hidden="true"></span>';
sectionHeader.addEventListener("click", () => toggleTodoSection(key, wrapper));
sectionHeader.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleTodoSection(key, wrapper); }
});
let listContainer = makeElement("div", wrapper, { className: "todo-group-list" }); let listContainer = makeElement("div", wrapper, { className: "todo-group-list" });
listContainer.style = "display:flex;flex-direction:column;gap:10px;"; listContainer.style = "display:flex;flex-direction:column;gap:10px;";
@ -3840,6 +3857,8 @@ async function createTodoSections(location) {
populateAssignments(true); populateAssignments(true);
} }
refreshTodoSections();
const feedbackElement = location.querySelector(".recent_feedback"); const feedbackElement = location.querySelector(".recent_feedback");
// populate progress rings placeholder (respect user toggle) // populate progress rings placeholder (respect user toggle)
@ -3954,6 +3973,61 @@ function clearTodoList() {
}); });
} }
/*
Collapsible due-date sections
The collapse state is session-only by default: it lives in todoCollapsedSections and
dies with the reload. "Remember collapsed sections" (todo_sections_persist, off by
default) seeds the map from chrome.storage once per load and writes every toggle
back, so collapses survive reloads only while that checkbox is on.
*/
let todoCollapsedSections = {};
let todoCollapsedSeeded = false;
function seedTodoCollapsedSections() {
if (todoCollapsedSeeded) return;
todoCollapsedSeeded = true;
if (options.todo_sections_persist === true && options.todo_collapsed_sections && typeof options.todo_collapsed_sections === "object") {
todoCollapsedSections = { ...options.todo_collapsed_sections };
}
}
function toggleTodoSection(key, wrapper) {
const collapsed = !wrapper.classList.contains("better-todo-collapsed");
applyTodoSectionCollapsed(key, wrapper, collapsed);
if (collapsed) todoCollapsedSections[key] = true;
else delete todoCollapsedSections[key];
if (options.todo_sections_persist === true) {
chrome.storage.sync.set({ "todo_collapsed_sections": { ...todoCollapsedSections } });
}
}
function applyTodoSectionCollapsed(key, wrapper, collapsed) {
wrapper.classList.toggle("better-todo-collapsed", collapsed);
const header = wrapper.querySelector(".better-todo-section-header");
if (header) header.setAttribute("aria-expanded", collapsed ? "false" : "true");
}
// Re-apply collapse state and refresh the per-section item counts after each render.
// Headers are rebuilt on every render, so this is what carries the state across the
// clearTodoList/createTodoSections cycle.
function refreshTodoSections() {
seedTodoCollapsedSections();
Object.keys(domContainers).forEach(key => {
const entry = domContainers[key];
if (!entry || !entry.wrapper || !entry.wrapper.isConnected) return;
const collapsed = todoCollapsedSections[key] === true;
applyTodoSectionCollapsed(key, entry.wrapper, collapsed);
// Every row the section holds (hidden "view more" rows included). The rows are
// created with a `class:` property assignment that never becomes a real class
// attribute, so query the list's children instead of the row classes; the
// celebration preview's disposable card must not bump the count.
const count = [...entry.listContainer.children].filter(el => el.id !== "canvasrefined-celebration-preview").length;
const countEl = entry.wrapper.querySelector(".better-todo-section-count");
if (countEl) countEl.textContent = count > 0 ? String(count) : "";
});
}
// "Alternate colors" (light mode only): icon fill goes white for lighter course strips. // "Alternate colors" (light mode only): icon fill goes white for lighter course strips.
const TODO_ALT_ICON_COLOR = "#ffffff"; const TODO_ALT_ICON_COLOR = "#ffffff";
let todoAltStyleEl = null; let todoAltStyleEl = null;
@ -5026,9 +5100,12 @@ function runCelebrationPreview() {
wrapper: todayContainer.wrapper, wrapper: todayContainer.wrapper,
display: todayContainer.wrapper.style.display, display: todayContainer.wrapper.style.display,
hadVisible: todayContainer.wrapper.hasAttribute("data-has-visible"), hadVisible: todayContainer.wrapper.hasAttribute("data-has-visible"),
wasCollapsed: todayContainer.wrapper.classList.contains("better-todo-collapsed"),
}; };
todayContainer.wrapper.style.display = "block"; todayContainer.wrapper.style.display = "block";
todayContainer.wrapper.setAttribute("data-has-visible", "true"); todayContainer.wrapper.setAttribute("data-has-visible", "true");
// A collapsed section would hide the preview card; expand it for the run.
todayContainer.wrapper.classList.remove("better-todo-collapsed");
} else { } else {
card.style.cssText += "position:fixed;top:70px;right:16px;width:300px;z-index:2147483646;box-shadow:0 2px 10px rgba(0,0,0,.25);"; card.style.cssText += "position:fixed;top:70px;right:16px;width:300px;z-index:2147483646;box-shadow:0 2px 10px rgba(0,0,0,.25);";
document.body.appendChild(card); document.body.appendChild(card);
@ -5046,6 +5123,7 @@ function runCelebrationPreview() {
if (restoreWrapper) { if (restoreWrapper) {
try { try {
restoreWrapper.wrapper.style.display = restoreWrapper.display; restoreWrapper.wrapper.style.display = restoreWrapper.display;
restoreWrapper.wrapper.classList.toggle("better-todo-collapsed", restoreWrapper.wasCollapsed);
if (!restoreWrapper.hadVisible) restoreWrapper.wrapper.removeAttribute("data-has-visible"); if (!restoreWrapper.hadVisible) restoreWrapper.wrapper.removeAttribute("data-has-visible");
} catch (e) { /* wrapper re-rendered away already */ } } catch (e) { /* wrapper re-rendered away already */ }
} }

View File

@ -55,6 +55,7 @@ const syncedSubOptions = [
"assignment_date_format", "assignment_date_format",
"todo_hr24", "todo_hr24",
"todo_separate_scrollbar", "todo_separate_scrollbar",
"todo_sections_persist",
"todo_alternate_colors", "todo_alternate_colors",
"todo_ignore_card_colors", "todo_ignore_card_colors",
"todo_remove_icons", "todo_remove_icons",
@ -220,7 +221,7 @@ const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"];
const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "imageRoundness", "cardSpacing", "cardWidth", "cardHeight", "cardPadding", "card_grid", "card_grid_columns", "card_grid_rows", "card_grid_column_gap", "card_grid_row_gap", "card_grid_center_rows", "card_grid_flex"]; const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "imageRoundness", "cardSpacing", "cardWidth", "cardHeight", "cardPadding", "card_grid", "card_grid_columns", "card_grid_rows", "card_grid_column_gap", "card_grid_row_gap", "card_grid_center_rows", "card_grid_flex"];
const exportLayout = ["full_width", "center_cards", "condensed_cards", "equal_height_cards", "remlogo", "hide_new_canvas", "hide_navbar", "hide_sidebar", "tab_icons"]; const exportLayout = ["full_width", "center_cards", "condensed_cards", "equal_height_cards", "remlogo", "hide_new_canvas", "hide_navbar", "hide_sidebar", "tab_icons"];
const exportSidebar = ["better_sidebar", "sidebar_scale"]; const exportSidebar = ["better_sidebar", "sidebar_scale"];
const exportTodo = ["better_todo", "todo_hide_feedback", "todo_hide_read", "todo_full_height", "todo_celebrations_enabled", "todo_celebrations", "todo_celebration_amount", "todo_progress_rings", "todo_timeframe", "todo_timeframe_custom_past", "todo_timeframe_custom_future", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", "todo_show_scores", "hover_preview"]; const exportTodo = ["better_todo", "todo_hide_feedback", "todo_hide_read", "todo_full_height", "todo_celebrations_enabled", "todo_celebrations", "todo_celebration_amount", "todo_progress_rings", "todo_timeframe", "todo_timeframe_custom_past", "todo_timeframe_custom_future", "todo_hr24", "todo_separate_scrollbar", "todo_alternate_colors", "todo_ignore_card_colors", "todo_remove_icons", "todo_show_scores", "hover_preview", "todo_sections_persist"];
const exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_calc_weighted", "gpa_calc_show_on_hover", "gpa_calc_align"]; const exportGpa = ["gpa_calc", "gpa_calc_prepend", "gpa_calc_cumulative", "gpa_calc_weighted", "gpa_calc_show_on_hover", "gpa_calc_align"];
// Widgets tab: every dashboard widget's layout (toggles, positions, heights, row // Widgets tab: every dashboard widget's layout (toggles, positions, heights, row
// heights, order, timer presets, quick link sizing). Personal data (quick link items, // heights, order, timer presets, quick link sizing). Personal data (quick link items,
@ -335,6 +336,7 @@ const defaultOptions = {
"card_opacity": 80, "card_opacity": 80,
"card_blur": 8, "card_blur": 8,
"todo_hr24": false, "todo_hr24": false,
"todo_sections_persist": false,
"anon_grade_level": "high", "anon_grade_level": "high",
"anon_grade_seed": 0, "anon_grade_seed": 0,
"anon_fake_todo": false, "anon_fake_todo": false,
@ -2055,6 +2057,7 @@ async function setup() {
"custom_font_skip_p", "custom_font_skip_p",
"todo_hr24", "todo_hr24",
"todo_separate_scrollbar", "todo_separate_scrollbar",
"todo_sections_persist",
"todo_alternate_colors", "todo_alternate_colors",
"todo_ignore_card_colors", "todo_ignore_card_colors",
"todo_remove_icons", "todo_remove_icons",