patches for firefox and other bugs

This commit is contained in:
Guy Sandler 2026-08-16 13:42:47 -07:00
parent b84a374099
commit 1091f01f83
8 changed files with 424 additions and 93 deletions

View File

@ -48,7 +48,7 @@
} }
.canvasrefined-add-assignment {max-height: 0; overflow:hidden;transition: .3s max-height;} .canvasrefined-add-assignment {max-height: 0; overflow:hidden;transition: .3s max-height;}
.canvasrefined-custom-open {max-height: 250px} .canvasrefined-custom-open {max-height: 250px}
#better-todo-add-task-menu.canvasrefined-custom-open {max-height: 420px;} #better-todo-add-task-menu.canvasrefined-custom-open {max-height: 520px;}
.canvasrefined-custom-input {box-sizing: border-box!important; width: 100%!important; height: 32px!important} .canvasrefined-custom-input {box-sizing: border-box!important; width: 100%!important; height: 32px!important}
#better-todo-new-task-date, #better-todo-new-task-date,
#better-todo-new-task-time { #better-todo-new-task-time {
@ -58,10 +58,9 @@
font-size: 12px; font-size: 12px;
padding-inline: 6px; padding-inline: 6px;
} }
#better-todo-new-task-date::-webkit-calendar-picker-indicator { /* The native date/time picker icons are themed in dark mode only via
filter: invert(1) brightness(2); color-scheme (see DARKMODE_CSS in css/darkmodecss.js); light mode keeps the
opacity: .95; default dark glyphs on the light surface. */
}
.canvasrefined-custom-btn {background: #f5f5f5; border: 1px solid #c7cdd1; border-radius:6px; padding: 2px 8px;} .canvasrefined-custom-btn {background: #f5f5f5; border: 1px solid #c7cdd1; border-radius:6px; padding: 2px 8px;}
/* Better sidebar buttons sit on the --bcsidebar surface and use --bcsidebar-text /* Better sidebar buttons sit on the --bcsidebar surface and use --bcsidebar-text
for labels/icons, so drop the light button chrome and use a subtle hover tint. */ for labels/icons, so drop the light button chrome and use a subtle hover tint. */
@ -204,7 +203,7 @@
.canvasrefined-notes-rendered ul,.canvasrefined-notes-rendered ol {margin:8px 0;padding-left:24px;} .canvasrefined-notes-rendered ul,.canvasrefined-notes-rendered ol {margin:8px 0;padding-left:24px;}
.canvasrefined-notes-rendered li {margin:3px 0;} .canvasrefined-notes-rendered li {margin:3px 0;}
.canvasrefined-notes-rendered li.cr-task {list-style:none;margin-left:-18px;display:flex;align-items:flex-start;gap:6px;} .canvasrefined-notes-rendered li.cr-task {list-style:none;margin-left:-18px;display:flex;align-items:flex-start;gap:6px;}
.canvasrefined-notes-rendered li.cr-task input {margin-top:4px;} .canvasrefined-notes-rendered li.cr-task input {margin-top:4px;cursor:pointer;}
.canvasrefined-notes-rendered blockquote {margin:8px 0;padding:4px 12px;border-left:3px solid var(--bcborders,#c7cdd1);color:var(--bctext-1,#5b6770);background:var(--bcbackground-2,rgba(0,0,0,.03));} .canvasrefined-notes-rendered blockquote {margin:8px 0;padding:4px 12px;border-left:3px solid var(--bcborders,#c7cdd1);color:var(--bctext-1,#5b6770);background:var(--bcbackground-2,rgba(0,0,0,.03));}
.canvasrefined-notes-rendered code {font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em;background:var(--bcbackground-2,#f1f3f5);padding:1px 5px;border-radius:3px;} .canvasrefined-notes-rendered code {font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em;background:var(--bcbackground-2,#f1f3f5);padding:1px 5px;border-radius:3px;}
.canvasrefined-notes-rendered pre {margin:10px 0;padding:10px 12px;background:var(--bcbackground-2,#f1f3f5);border:1px solid var(--bcborders,#e2e6e8);border-radius:4px;overflow:auto;} .canvasrefined-notes-rendered pre {margin:10px 0;padding:10px 12px;background:var(--bcbackground-2,#f1f3f5);border:1px solid var(--bcborders,#e2e6e8);border-radius:4px;overflow:auto;}

View File

@ -1193,4 +1193,18 @@ hr {
.event-details strong { .event-details strong {
color: var(--bctext-0) !important; color: var(--bctext-0) !important;
} }
/* Theme the native date/time picker icons so they stay visible on the dark
sidebar surface. Applied in dark mode only light mode keeps the default
dark glyphs (see css/content.css).
color-scheme: dark is the cross-browser way to flip the native picker icon
to a light glyph. Firefox 109+ ignores ::-webkit-calendar-picker-indicator
(the icon lives in a closed Shadow DOM), so a filter alone does nothing
there; color-scheme fixes both Firefox and modern Chrome. Do NOT also apply
an invert filter color-scheme already makes the glyph light, and inverting
would flip it back to dark in Chrome. */
#better-todo-new-task-date,
#better-todo-new-task-time {
color-scheme: dark !important;
color: var(--bctext-0) !important;
}
`; `;

View File

@ -500,7 +500,13 @@
<div id="saved-themes"></div> <div id="saved-themes"></div>
</div> </div>
<div class="export-details option-container"> <div class="export-details option-container">
<h3 class="header-small">Export Settings</h3> <h3 class="header-small" style="display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap;">
<span>Export Settings</span>
<span style="display:flex;gap:6px;">
<button type="button" id="export-check-all" class="customization-button" style="padding:4px 10px;font-size:11px;">Check all</button>
<button type="button" id="export-check-none" class="customization-button" style="padding:4px 10px;font-size:11px;">Check none</button>
</span>
</h3>
<div style="display:flex;gap:30px;"> <div style="display:flex;gap:30px;">
<div class="export-inputs"> <div class="export-inputs">
<div class="sub-option"> <div class="sub-option">

View File

@ -81,6 +81,8 @@ chrome.runtime.onInstalled.addListener(function () {
"tab_icons": false, "tab_icons": false,
"todo_hide_feedback": false, "todo_hide_feedback": false,
"todo_full_height": true, "todo_full_height": true,
"todo_progress_rings": true,
"todo_confetti": true,
"device_dark": false, "device_dark": false,
"cumulative_gpa": { "name": "Cumulative GPA", "hidden": false, "weight": "dnc", "credits": 999, "gr": 3.21 }, "cumulative_gpa": { "name": "Cumulative GPA", "hidden": false, "weight": "dnc", "credits": 999, "gr": 3.21 },
// "show_updates": false, // "show_updates": false,

View File

@ -284,9 +284,7 @@ function createNasaInfoOverlay() {
const panel = nasaInfoOverlayEl.querySelector("#nasa-info-panel"); const panel = nasaInfoOverlayEl.querySelector("#nasa-info-panel");
const populatePanel = async () => { const populatePanel = async () => {
// The NASA worker (background.js getNasaBackground) falls back up to 7 days // Search backward up to 7 days for the APOD actually cached, since today's may not be ready.
// when today's APOD isn't available yet, so search backward for the date
// that was actually cached and displayed instead of assuming today.
const date = new Date(); const date = new Date();
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
const dateStr = date.toISOString().slice(0, 10); const dateStr = date.toISOString().slice(0, 10);
@ -620,8 +618,7 @@ function applyOptionsChanges(changes) {
loadCardAssignments(); loadCardAssignments();
break; break;
case "equal_height_cards": case "equal_height_cards":
// No need to rebuild the assignment rows — just stretch (or reset) // Stretch or reset card heights in place instead of rebuilding rows.
// the card heights in place for a snappy toggle.
equalizeCardHeights(); equalizeCardHeights();
break; break;
case "custom_cards": case "custom_cards":
@ -833,7 +830,7 @@ async function applyCustomBackground() {
-webkit-backdrop-filter: blur(8px) saturate(120%) !important; -webkit-backdrop-filter: blur(8px) saturate(120%) !important;
} }
#right-side-wrapper { #right-side-wrapper {
// backdrop-filter: blur(10px) !important; /* backdrop-filter: blur(10px) !important; */
background-color: color-mix(in srgb, var(--bcbackground-0), transparent 35%); background-color: color-mix(in srgb, var(--bcbackground-0), transparent 35%);
border-radius: 5px; border-radius: 5px;
} }
@ -1553,6 +1550,7 @@ function buildPlannerNotePayload(form) {
const courseIdRaw = form.querySelector("#better-todo-new-task-course")?.value; const courseIdRaw = form.querySelector("#better-todo-new-task-course")?.value;
const dateValue = form.querySelector("#better-todo-new-task-date")?.value; const dateValue = form.querySelector("#better-todo-new-task-date")?.value;
const timeValue = form.querySelector("#better-todo-new-task-time")?.value; const timeValue = form.querySelector("#better-todo-new-task-time")?.value;
const link = form.querySelector("#better-todo-new-task-link")?.value?.trim() || "";
if (!title) { if (!title) {
throw new Error("Task title is required."); throw new Error("Task title is required.");
@ -1570,6 +1568,7 @@ function buildPlannerNotePayload(form) {
return { return {
title, title,
details, details,
link,
courseId: courseIdRaw ? parseInt(courseIdRaw) : null, courseId: courseIdRaw ? parseInt(courseIdRaw) : null,
// Canvas accepts local timestamp strings more reliably than UTC ISO strings for planner notes. // Canvas accepts local timestamp strings more reliably than UTC ISO strings for planner notes.
todoDate: `${dateValue}T${timeValue}:00`, todoDate: `${dateValue}T${timeValue}:00`,
@ -1646,6 +1645,167 @@ async function createCanvasPlannerNote(payload) {
throw new Error(lastError || "Canvas rejected task creation."); throw new Error(lastError || "Canvas rejected task creation.");
} }
/* Custom task links: Canvas planner notes have no link field, so store a
user link in sync storage keyed by the note id. */
function getCustomTaskLinks() {
return (options && options.custom_task_links) || {};
}
function getCustomTaskLinkId(item) {
return item?.plannable_id ?? item?.plannable?.id ?? null;
}
function normalizeTaskLink(link) {
if (!link) return "";
link = String(link).trim();
if (!link) return "";
if (/^https?:\/\//i.test(link)) return link;
if (link.startsWith("//")) return "https:" + link;
return domain + (link.startsWith("/") ? link : "/" + link);
}
function customTaskHref(item) {
const id = getCustomTaskLinkId(item);
const links = getCustomTaskLinks();
if (id != null && links[String(id)]) {
return normalizeTaskLink(links[String(id)]);
}
const courseId = item?.course_id || item?.plannable?.course_id || item?.context_id;
if (courseId) return `${domain}/courses/${courseId}`;
return `${domain}/`;
}
function saveCustomTaskLink(noteId, link) {
if (noteId == null) return;
const links = { ...getCustomTaskLinks() };
const key = String(noteId);
if (link && String(link).trim()) {
links[key] = String(link).trim();
} else {
delete links[key];
}
options = { ...options, custom_task_links: links };
chrome.storage.sync.set({ custom_task_links: links });
}
function deleteCustomTaskLink(noteId) {
if (noteId == null) return;
const links = { ...getCustomTaskLinks() };
delete links[String(noteId)];
options = { ...options, custom_task_links: links };
chrome.storage.sync.set({ custom_task_links: links });
}
async function updateCanvasPlannerNote(id, payload) {
if (!id) throw new Error("Missing task id.");
const csrfToken = CSRFtoken();
const plannerNote = {
title: payload.title,
todo_date: payload.todoDate,
};
if (payload.details) plannerNote.details = payload.details;
// Sending course_id as empty string disassociates the note from its course.
plannerNote.course_id = payload.courseId || "";
const attempts = [
{
headers: {
"content-type": "application/json",
"accept": "application/json",
"X-CSRF-Token": csrfToken,
},
body: JSON.stringify({ planner_note: plannerNote }),
},
{
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
"accept": "application/json",
"X-CSRF-Token": csrfToken,
},
body: (() => {
const formBody = new URLSearchParams();
formBody.set("planner_note[title]", plannerNote.title);
formBody.set("planner_note[todo_date]", plannerNote.todo_date);
if (plannerNote.details) formBody.set("planner_note[details]", plannerNote.details);
formBody.set("planner_note[course_id]", plannerNote.course_id);
return formBody.toString();
})(),
},
];
let lastError = "Canvas rejected task update.";
for (const attempt of attempts) {
const response = await fetch(`${domain}/api/v1/planner_notes/${id}`, {
method: "PUT",
headers: attempt.headers,
body: attempt.body,
});
if (response.status === 200 || response.status === 201) {
return response.json();
}
try {
const errData = await response.json();
if (errData?.errors?.length) {
lastError = errData.errors.join(" ");
} else if (errData?.message) {
lastError = errData.message;
}
} catch (_) {
// Keep prior error text when body is not JSON.
}
}
throw new Error(lastError || "Canvas rejected task update.");
}
async function deleteCanvasPlannerNote(id) {
if (!id) throw new Error("Missing task id.");
const csrfToken = CSRFtoken();
const response = await fetch(`${domain}/api/v1/planner_notes/${id}`, {
method: "DELETE",
headers: {
"content-type": "application/json",
"accept": "application/json",
"X-CSRF-Token": csrfToken,
},
});
if (response.status === 200 || response.status === 201 || response.status === 204) {
return true;
}
let lastError = "Canvas rejected task deletion.";
try {
const errData = await response.json();
if (errData?.errors?.length) lastError = errData.errors.join(" ");
else if (errData?.message) lastError = errData.message;
} catch (_) { /* ignore */ }
throw new Error(lastError);
}
/* Scroll a task form field into view, leaving room below for the native date/time picker. */
function scrollTodoIntoView(el, smooth = true) {
if (!el) return;
const sidebar = document.getElementById("right-side-wrapper");
const style = sidebar ? getComputedStyle(sidebar) : null;
const scrollsSidebar = sidebar &&
(style.overflowY === "auto" || style.overflowY === "scroll") &&
sidebar.scrollHeight > sidebar.clientHeight;
const behavior = smooth ? "smooth" : "auto";
if (scrollsSidebar) {
const rect = el.getBoundingClientRect();
const sRect = sidebar.getBoundingClientRect();
const elTop = rect.top - sRect.top + sidebar.scrollTop;
// Keep the element near the top so the picker below it stays on screen.
const target = elTop - (sidebar.clientHeight - rect.height) * 0.3;
sidebar.scrollTo({ top: Math.max(0, target), behavior });
} else {
el.scrollIntoView({ block: "center", behavior });
}
}
function fillTaskCourseOptions(courseSelect) { function fillTaskCourseOptions(courseSelect) {
const cards = options.custom_cards || {}; const cards = options.custom_cards || {};
const courseColors = options.custom_cards_3 || {}; const courseColors = options.custom_cards_3 || {};
@ -1705,7 +1865,8 @@ function ensureTodoTaskMenu(location, feedbackElement) {
}); });
menu.innerHTML = ` menu.innerHTML = `
<div style="display:flex;flex-direction:column;gap:8px;padding:8px;border:1px solid #c7cdd1;border-radius:6px;background:var(--bcbackground-2);"> <div style="display:flex;flex-direction:column;gap:8px;padding:8px;border:1px solid #c7cdd1;border-radius:6px;background:var(--bcbackground-2);position:relative;">
<button id="better-todo-add-task-close" type="button" class="canvasrefined-custom-btn" title="Close" style="position:absolute;top:4px;right:6px;padding:0 6px;cursor:pointer;line-height:18px;font-size:14px;color:var(--bctext-1);">\u00d7</button>
<input type="text" id="better-todo-new-task-title" class="canvasrefined-custom-input" placeholder="Task title" maxlength="255"> <input type="text" id="better-todo-new-task-title" class="canvasrefined-custom-input" placeholder="Task title" maxlength="255">
<textarea id="better-todo-new-task-details" class="canvasrefined-custom-input" placeholder="Details (optional)" style="min-height:70px;resize:vertical;padding-top:6px;padding-bottom:6px;"></textarea> <textarea id="better-todo-new-task-details" class="canvasrefined-custom-input" placeholder="Details (optional)" style="min-height:70px;resize:vertical;padding-top:6px;padding-bottom:6px;"></textarea>
<select id="better-todo-new-task-course" class="canvasrefined-custom-input"></select> <select id="better-todo-new-task-course" class="canvasrefined-custom-input"></select>
@ -1713,9 +1874,13 @@ function ensureTodoTaskMenu(location, feedbackElement) {
<input type="date" id="better-todo-new-task-date" class="canvasrefined-custom-input"> <input type="date" id="better-todo-new-task-date" class="canvasrefined-custom-input">
<input type="time" id="better-todo-new-task-time" class="canvasrefined-custom-input"> <input type="time" id="better-todo-new-task-time" class="canvasrefined-custom-input">
</div> </div>
<input type="text" id="better-todo-new-task-link" class="canvasrefined-custom-input" placeholder="Link (optional)" maxlength="2048">
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;"> <div style="display:flex;justify-content:space-between;align-items:center;gap:8px;">
<span id="better-todo-add-task-status" style="font-size:12px;color:var(--bctext-0);"></span> <span id="better-todo-add-task-status" style="font-size:12px;color:var(--bctext-0);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>
<button id="better-todo-add-task-submit" class="canvasrefined-custom-btn" style="padding:4px 10px;cursor:pointer;" type="button">Create</button> <div style="display:flex;gap:6px;align-items:center;">
<button id="better-todo-add-task-delete" class="canvasrefined-custom-btn" style="padding:4px 10px;cursor:pointer;display:none;color:#db3754;" type="button" title="Delete this custom task">Delete</button>
<button id="better-todo-add-task-submit" class="canvasrefined-custom-btn" style="padding:4px 10px;cursor:pointer;" type="button">Create</button>
</div>
</div> </div>
</div> </div>
`; `;
@ -1729,32 +1894,90 @@ function ensureTodoTaskMenu(location, feedbackElement) {
courseSelect.addEventListener("change", () => updateTaskCourseSelectColor(courseSelect)); courseSelect.addEventListener("change", () => updateTaskCourseSelectColor(courseSelect));
addTaskButton.addEventListener("click", () => { addTaskButton.addEventListener("click", () => {
const willOpen = !menu.classList.contains("canvasrefined-custom-open");
menu.classList.toggle("canvasrefined-custom-open"); menu.classList.toggle("canvasrefined-custom-open");
if (willOpen) {
resetTaskFormToCreate(menu);
// Scroll the form up so the picker stays on screen.
scrollTodoIntoView(menu, true);
}
}); });
menu.querySelector("#better-todo-add-task-submit").addEventListener("click", async () => { const submitTask = async () => {
const status = menu.querySelector("#better-todo-add-task-status"); const status = menu.querySelector("#better-todo-add-task-status");
const submitButton = menu.querySelector("#better-todo-add-task-submit"); const submitButton = menu.querySelector("#better-todo-add-task-submit");
const deleteButton = menu.querySelector("#better-todo-add-task-delete");
status.textContent = ""; status.textContent = "";
submitButton.disabled = true; submitButton.disabled = true;
if (deleteButton) deleteButton.disabled = true;
try { try {
const payload = buildPlannerNotePayload(menu); const payload = buildPlannerNotePayload(menu);
await createCanvasPlannerNote(payload); const editingId = menu.dataset.editingId || null;
status.textContent = "Task created."; if (editingId) {
await updateCanvasPlannerNote(editingId, payload);
saveCustomTaskLink(editingId, payload.link);
status.textContent = "Task updated.";
} else {
const created = await createCanvasPlannerNote(payload);
const newId = created?.id;
if (newId != null) saveCustomTaskLink(newId, payload.link);
status.textContent = "Task created.";
}
status.style.color = "#198754"; status.style.color = "#198754";
menu.querySelector("#better-todo-new-task-title").value = ""; resetTaskFormToCreate(menu);
menu.querySelector("#better-todo-new-task-details").value = "";
menu.classList.remove("canvasrefined-custom-open"); menu.classList.remove("canvasrefined-custom-open");
getAssignments(); getAssignments();
clearTodoList(); clearTodoList();
createTodoSections(location); createTodoSections(location);
} catch (e) { } catch (e) {
status.textContent = e?.message || "Could not create task."; status.textContent = e?.message || "Could not save task.";
status.style.color = "#db3754"; status.style.color = "#db3754";
} finally { } finally {
submitButton.disabled = false; submitButton.disabled = false;
if (deleteButton) deleteButton.disabled = false;
}
};
menu.querySelector("#better-todo-add-task-submit").addEventListener("click", submitTask);
// Close (×) button: dismiss the form without creating/editing a task.
menu.querySelector("#better-todo-add-task-close")?.addEventListener("click", () => {
resetTaskFormToCreate(menu);
menu.classList.remove("canvasrefined-custom-open");
});
// Reposition the field on focus so the picker opens on screen.
["#better-todo-new-task-date", "#better-todo-new-task-time"].forEach((sel) => {
const input = menu.querySelector(sel);
input?.addEventListener("focus", () => scrollTodoIntoView(input, false));
});
menu.querySelector("#better-todo-add-task-delete").addEventListener("click", async () => {
const editingId = menu.dataset.editingId || null;
if (!editingId) return;
if (!confirm("Delete this custom task? This cannot be undone.")) return;
const status = menu.querySelector("#better-todo-add-task-status");
const submitButton = menu.querySelector("#better-todo-add-task-submit");
const deleteButton = menu.querySelector("#better-todo-add-task-delete");
status.textContent = "";
submitButton.disabled = true;
deleteButton.disabled = true;
try {
await deleteCanvasPlannerNote(editingId);
deleteCustomTaskLink(editingId);
resetTaskFormToCreate(menu);
menu.classList.remove("canvasrefined-custom-open");
getAssignments();
clearTodoList();
createTodoSections(location);
} catch (e) {
status.textContent = e?.message || "Could not delete task.";
status.style.color = "#db3754";
} finally {
submitButton.disabled = false;
deleteButton.disabled = false;
} }
}); });
} }
@ -1768,6 +1991,64 @@ function ensureTodoTaskMenu(location, feedbackElement) {
} }
} }
// Reset the shared add/edit task form back to a blank "create" state.
function resetTaskFormToCreate(menu) {
if (!menu) return;
menu.querySelector("#better-todo-new-task-title").value = "";
menu.querySelector("#better-todo-new-task-details").value = "";
menu.querySelector("#better-todo-new-task-link").value = "";
const courseSelect = menu.querySelector("#better-todo-new-task-course");
if (courseSelect) {
fillTaskCourseOptions(courseSelect);
updateTaskCourseSelectColor(courseSelect);
}
const now = new Date();
menu.querySelector("#better-todo-new-task-date").value = formatDateForInput(now);
menu.querySelector("#better-todo-new-task-time").value = formatTimeForInput(now);
const status = menu.querySelector("#better-todo-add-task-status");
if (status) { status.textContent = ""; status.style.color = ""; }
const del = menu.querySelector("#better-todo-add-task-delete");
if (del) del.style.display = "none";
const submit = menu.querySelector("#better-todo-add-task-submit");
if (submit) submit.textContent = "Create";
delete menu.dataset.editingId;
}
// Open the shared form pre-filled with a custom task for editing or deletion.
function openTaskForEdit(item) {
const location = document.getElementById("canvasrefined-todo-list");
if (!location) return;
const feedbackElement = location.querySelector(".recent_feedback");
ensureTodoTaskMenu(location, feedbackElement);
const menu = document.getElementById("better-todo-add-task-menu");
if (!menu) return;
const noteId = getCustomTaskLinkId(item);
menu.querySelector("#better-todo-new-task-title").value = item?.plannable?.title || "";
menu.querySelector("#better-todo-new-task-details").value = item?.plannable?.details || "";
const courseSelect = menu.querySelector("#better-todo-new-task-course");
const cid = item?.course_id || item?.plannable?.course_id || "";
if (courseSelect) courseSelect.value = cid ? String(cid) : "";
updateTaskCourseSelectColor(courseSelect);
const dateObj = new Date(item?.plannable_date || item?.plannable?.todo_date || Date.now());
if (!Number.isNaN(dateObj.getTime())) {
menu.querySelector("#better-todo-new-task-date").value = formatDateForInput(dateObj);
menu.querySelector("#better-todo-new-task-time").value = formatTimeForInput(dateObj);
}
const links = getCustomTaskLinks();
menu.querySelector("#better-todo-new-task-link").value = (noteId != null && links[String(noteId)]) || "";
menu.dataset.editingId = noteId != null ? String(noteId) : "";
const del = menu.querySelector("#better-todo-add-task-delete");
if (del) del.style.display = "";
menu.querySelector("#better-todo-add-task-submit").textContent = "Save";
const status = menu.querySelector("#better-todo-add-task-status");
if (status) { status.textContent = ""; status.style.color = ""; }
menu.classList.add("canvasrefined-custom-open");
scrollTodoIntoView(menu, true);
}
async function createTodoSections(location) { async function createTodoSections(location) {
if (!location.querySelector("#better-todo-header")) { if (!location.querySelector("#better-todo-header")) {
let header = makeElement("div", location, { id: "better-todo-header" }); let header = makeElement("div", location, { id: "better-todo-header" });
@ -2071,6 +2352,10 @@ function populateAssignments(iscompleted = false) {
"#cccccc"; "#cccccc";
const isCustomTask = item.plannable_type == "planner_note" || item.planner_override?.custom === true; const isCustomTask = item.plannable_type == "planner_note" || item.planner_override?.custom === true;
const taskHref = isCustomTask ? customTaskHref(item) : (domain + item.html_url);
const editButtonSvg = isCustomTask
? `<svg class="better-todo-assignment-edit" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:15px;height:15px;position:absolute;top:18px;right:5px;opacity:0.3;transition:all .3s ease;cursor:pointer;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.3'" title="Edit this custom task"><path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z" stroke="var(--bctext-0)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`
: "";
const iconSize = isCustomTask ? 26 : 20; const iconSize = isCustomTask ? 26 : 20;
const iconLeftOffset = isCustomTask ? 2 : 5; const iconLeftOffset = isCustomTask ? 2 : 5;
const taskIcon = isCustomTask const taskIcon = isCustomTask
@ -2095,9 +2380,10 @@ function populateAssignments(iscompleted = false) {
<div style="width:calc(100% - 40px);height:80%;display:flex;flex-direction:column;gap:5px;padding-left:2px;box-sizing:border-box;overflow:hidden;position:relative;"> <div style="width:calc(100% - 40px);height:80%;display:flex;flex-direction:column;gap:5px;padding-left:2px;box-sizing:border-box;overflow:hidden;position:relative;">
<div style="display:flex;flex-direction:column;gap:3px;"> <div style="display:flex;flex-direction:column;gap:3px;">
<span style="color:${courseColor};font-size:12px;margin-top:-2px;">${item.context_name}</span> <span style="color:${courseColor};font-size:12px;margin-top:-2px;">${item.context_name}</span>
<a href="${domain + item.html_url}" style="color:inherit;text-decoration:none;font-weight:bold;text-overflow:ellipsis;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-5px;">${item.plannable.title}</a> <a href="${taskHref}" style="color:inherit;text-decoration:none;font-weight:bold;text-overflow:ellipsis;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-5px;">${item.plannable.title}</a>
<span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}</span> <span style="color:var(--bctext-0);font-size:12px;margin-top:-5px;">${convertToDueDate(item.plannable_date)}</span>
</div> </div>
${editButtonSvg}
<svg class="better-todo-assignment-checkmark" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:15px;height:15px;position:absolute;top:0px;right:5px;opacity:0.3;transition:all .3s ease;cursor:pointer;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.3'"> <svg class="better-todo-assignment-checkmark" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:15px;height:15px;position:absolute;top:0px;right:5px;opacity:0.3;transition:all .3s ease;cursor:pointer;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.3'">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g> <g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g> <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
@ -2112,6 +2398,14 @@ function populateAssignments(iscompleted = false) {
console.log("marking ", item.plannable.title, " as complete"); console.log("marking ", item.plannable.title, " as complete");
markAs(item, assignment.firstElementChild); markAs(item, assignment.firstElementChild);
}); });
const editBtn = assignment.querySelector(".better-todo-assignment-edit");
if (editBtn) {
editBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
openTaskForEdit(item);
});
}
}); });
if (document.getElementById("better-todo-see-more")) { if (document.getElementById("better-todo-see-more")) {
@ -3056,11 +3350,7 @@ async function changeColorPreset(colors) {
Dark mode Dark mode
*/ */
// Light-mode fallback values for the --bc* theme variables. These are always // Light-mode fallbacks for the --bc* variables, always emitted so extension UI renders in light mode; dark mode overrides below.
// emitted (even when dark mode is OFF) so extension UI referencing
// var(--bctext-0) / var(--bcborders) / var(--bcbackground-*) still renders
// correctly in light mode (e.g. better sidebar/todo icons stay dark instead
// of falling back to the SVG initial values). Dark mode overrides these below.
const BC_LIGHT_DEFAULTS = { const BC_LIGHT_DEFAULTS = {
"background-0": "#ffffff", "background-0": "#ffffff",
"background-1": "#c7c7c7", "background-1": "#c7c7c7",
@ -3264,12 +3554,8 @@ function createCardAssignment(assignment) {
let cardAssignments; let cardAssignments;
/* /* Equal Height Cards: stretch each card's assignment area to match the tallest
Equal Height Cards when enabled, every dashboard card's assignment area is one, using min-height so cards can still grow. */
stretched to match the tallest one so cards with fewer assignments don't end
up shorter than the rest. Uses min-height (never a fixed height) so cards can
still grow if their content exceeds the tallest card.
*/
let equalHeightResizeTimer = null; let equalHeightResizeTimer = null;
function equalizeCardHeights() { function equalizeCardHeights() {
@ -3278,8 +3564,7 @@ function equalizeCardHeights() {
const enabled = options.equal_height_cards === true && options.assignments_due === true; const enabled = options.equal_height_cards === true && options.assignments_due === true;
// Always clear any previously applied min-height first so we can either // Clear prior min-height so we can measure fresh or fully reset.
// measure natural heights (when enabling) or fully reset (when disabling).
cards.forEach(card => { cards.forEach(card => {
const area = card.querySelector(".canvasrefined-card-assignment"); const area = card.querySelector(".canvasrefined-card-assignment");
if (area) area.style.removeProperty("min-height"); if (area) area.style.removeProperty("min-height");
@ -3287,9 +3572,7 @@ function equalizeCardHeights() {
if (!enabled) return; if (!enabled) return;
// Measure the natural height of each card's assignment area, then stretch // Stretch each assignment area to the tallest one.
// every area to the tallest one. Card headers are uniform, so equalizing
// the assignment area makes all cards the same total height.
let maxHeight = 0; let maxHeight = 0;
cards.forEach(card => { cards.forEach(card => {
const area = card.querySelector(".canvasrefined-card-assignment"); const area = card.querySelector(".canvasrefined-card-assignment");
@ -3781,13 +4064,7 @@ function delayDashboardNotesStorage(text) {
}, 250); }, 250);
} }
/* /* Fallback Markdown renderer used only if js/markdown.js failed to load. */
Built-in fallback Markdown renderer. Used only if js/markdown.js failed to load
(window.renderMarkdown missing) so the notes still render formatted output instead
of showing raw markdown text. Covers the common subset: headings, bold/italic/strike,
inline + fenced code, links, images, lists, task lists, blockquotes, hr, paragraphs.
All user text is HTML-escaped before formatting.
*/
function crRenderMarkdownFallback(src) { function crRenderMarkdownFallback(src) {
if (src == null) return ""; if (src == null) return "";
const escapeHtml = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;"); const escapeHtml = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@ -3804,6 +4081,7 @@ function crRenderMarkdownFallback(src) {
const out = []; const out = [];
const lines = text.split("\n"); const lines = text.split("\n");
let i = 0; let i = 0;
let taskSeq = 0; // ordinal of rendered task items, stable across code-block stashing
const inline = (t) => { const inline = (t) => {
let h = escapeHtml(t); let h = escapeHtml(t);
h = h.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (m, alt, url, title) => h = h.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (m, alt, url, title) =>
@ -3827,7 +4105,7 @@ function crRenderMarkdownFallback(src) {
out.push(`<blockquote>${q.join("<br>")}</blockquote>`); continue; out.push(`<blockquote>${q.join("<br>")}</blockquote>`); continue;
} }
if (/^\s*[-*+]\s+/.test(line)) { if (/^\s*[-*+]\s+/.test(line)) {
const items = []; while (i < lines.length) { const m = lines[i].match(/^\s*[-*+]\s+(.*)$/); if (!m) break; const tk = m[1].match(/^\[([ xX])\]\s+(.*)$/); if (tk) { items.push(`<li class="cr-task"><input type="checkbox" disabled${/x/i.test(tk[1]) ? " checked" : ""}> ${inline(tk[2])}</li>`); } else { items.push(`<li>${inline(m[1])}</li>`); } i++; } out.push(`<ul>${items.join("")}</ul>`); continue; const items = []; while (i < lines.length) { const m = lines[i].match(/^\s*[-*+]\s+(.*)$/); if (!m) break; const tk = m[1].match(/^\[([ xX])\]\s+(.*)$/); if (tk) { items.push(`<li class="cr-task" data-cr-task="${taskSeq++}"><input type="checkbox"${/x/i.test(tk[1]) ? " checked" : ""}> ${inline(tk[2])}</li>`); } else { items.push(`<li>${inline(m[1])}</li>`); } i++; } out.push(`<ul>${items.join("")}</ul>`); continue;
} }
if (/^\s*\d+\.\s+/.test(line)) { if (/^\s*\d+\.\s+/.test(line)) {
const items = []; while (i < lines.length) { const m = lines[i].match(/^\s*\d+\.\s+(.*)$/); if (!m) break; items.push(`<li>${inline(m[1])}</li>`); i++; } out.push(`<ol>${items.join("")}</ol>`); continue; const items = []; while (i < lines.length) { const m = lines[i].match(/^\s*\d+\.\s+(.*)$/); if (!m) break; items.push(`<li>${inline(m[1])}</li>`); i++; } out.push(`<ol>${items.join("")}</ol>`); continue;
@ -3840,20 +4118,14 @@ function crRenderMarkdownFallback(src) {
function renderDashboardNotesPreview(preview, text) { function renderDashboardNotesPreview(preview, text) {
if (!preview) return; if (!preview) return;
// Skip identical re-renders: writing innerHTML is a childList mutation that the // Skip identical re-renders to avoid a self-sustaining observer loop.
// dashboard MutationObserver picks up, which re-calls loadDashboardNotes, which
// re-renders... Without this guard the notes box drives a tight self-sustaining
// loop (setTimeout 0) that starves the main thread so the render never paints.
if (preview._crLastText === text) return; if (preview._crLastText === text) return;
preview._crLastText = text; preview._crLastText = text;
const renderer = (typeof window.renderMarkdown === "function") ? window.renderMarkdown : crRenderMarkdownFallback; const renderer = (typeof window.renderMarkdown === "function") ? window.renderMarkdown : crRenderMarkdownFallback;
preview.innerHTML = renderer(text); preview.innerHTML = renderer(text);
} }
/* /* Insert/wrap Markdown formatting at the selection and fire an input event. */
Insert/wrap Markdown formatting in the notes editor at the current selection.
Dispatches a synthetic `input` event so the live preview + storage handler runs.
*/
function notesApplyFormat(editor, action) { function notesApplyFormat(editor, action) {
if (!editor) return; if (!editor) return;
const start = editor.selectionStart; const start = editor.selectionStart;
@ -3947,6 +4219,34 @@ function notesApplyFormat(editor, action) {
} }
} }
/* Toggle the Nth rendered Markdown task checkbox and re-render, using a task
ordinal that skips fenced code blocks. */
function toggleDashboardNoteTask(editor, taskIndex) {
if (!editor) return;
const value = editor.value || "";
const lines = value.split("\n");
let inFence = false;
let count = 0;
for (let idx = 0; idx < lines.length; idx++) {
const line = lines[idx];
if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
if (inFence) continue;
const m = line.match(/^(\s*[-*+]\s+)\[([ xX])\](\s+)/);
if (!m) continue;
if (count === taskIndex) {
const newMark = m[2] === " " ? "x" : " ";
lines[idx] = m[1] + "[" + newMark + "]" + m[3] + line.slice(m[0].length);
editor.value = lines.join("\n");
editor.dispatchEvent(new Event("input", { bubbles: true }));
const notes = editor.closest(".canvasrefined-dashboard-notes");
const rendered = notes ? notes.querySelector(".canvasrefined-notes-rendered") : null;
if (rendered) renderDashboardNotesPreview(rendered, editor.value);
return;
}
count++;
}
}
const DASHBOARD_NOTES_HTML = ` const DASHBOARD_NOTES_HTML = `
<div class="canvasrefined-notes-toolbar" role="toolbar" aria-label="Format notes"> <div class="canvasrefined-notes-toolbar" role="toolbar" aria-label="Format notes">
<button type="button" class="cr-fmt" data-action="bold" title="Bold (Ctrl/Cmd+B)"><strong>B</strong></button> <button type="button" class="cr-fmt" data-action="bold" title="Bold (Ctrl/Cmd+B)"><strong>B</strong></button>
@ -3990,7 +4290,18 @@ function wireDashboardNotes(notes) {
renderDashboardNotesPreview(rendered, editor.value); renderDashboardNotesPreview(rendered, editor.value);
}; };
rendered.addEventListener("click", enterEdit); rendered.addEventListener("click", (e) => {
// Clicking a checkbox toggles the task; clicking the text edits.
const t = e.target;
if (t && t.nodeType === 1 && t.tagName === "INPUT" && t.type === "checkbox" && t.closest && t.closest("li.cr-task")) {
e.stopPropagation();
const li = t.closest("li.cr-task");
const taskIndex = parseInt(li.dataset.crTask, 10);
if (!Number.isNaN(taskIndex)) toggleDashboardNoteTask(editor, taskIndex);
return;
}
enterEdit();
});
rendered.addEventListener("focus", enterEdit); rendered.addEventListener("focus", enterEdit);
editor.addEventListener("blur", exitEdit); editor.addEventListener("blur", exitEdit);
editor.addEventListener("input", function () { editor.addEventListener("input", function () {
@ -3998,11 +4309,10 @@ function wireDashboardNotes(notes) {
delayDashboardNotesStorage(this.value); delayDashboardNotesStorage(this.value);
}); });
// Toolbar buttons: keep focus in the editor (mousedown preventDefault stops the // preventDefault on the toolbar keeps focus in the editor across misclicks.
// button from stealing focus and collapsing back to the rendered view), then apply const toolbar = notes.querySelector(".canvasrefined-notes-toolbar");
// the formatting. Enters edit mode first if the user formats from the rendered view. if (toolbar) toolbar.addEventListener("mousedown", e => e.preventDefault());
notes.querySelectorAll(".cr-fmt").forEach(btn => { notes.querySelectorAll(".cr-fmt").forEach(btn => {
btn.addEventListener("mousedown", e => e.preventDefault());
btn.addEventListener("click", () => { btn.addEventListener("click", () => {
if (!notes.classList.contains("is-editing")) enterEdit(); if (!notes.classList.contains("is-editing")) enterEdit();
notesApplyFormat(editor, btn.dataset.action); notesApplyFormat(editor, btn.dataset.action);
@ -4044,9 +4354,7 @@ function loadDashboardNotes() {
notes.style.display = ""; notes.style.display = "";
const editor = notes.querySelector(".canvasrefined-notes-editor"); const editor = notes.querySelector(".canvasrefined-notes-editor");
const rendered = notes.querySelector(".canvasrefined-notes-rendered"); const rendered = notes.querySelector(".canvasrefined-notes-rendered");
// While editing, the textarea is the source of truth: don't clobber it from // Only sync and render in view mode; the textarea is the source of truth while editing.
// storage and don't waste a render on the hidden rendered view (which would
// also feed the observer loop). Only sync + render in view mode.
if (!notes.classList.contains("is-editing")) { if (!notes.classList.contains("is-editing")) {
if (editor && editor.value !== (options.dashboard_notes_text || "")) { if (editor && editor.value !== (options.dashboard_notes_text || "")) {
editor.value = options.dashboard_notes_text || ""; editor.value = options.dashboard_notes_text || "";
@ -4134,9 +4442,7 @@ function changeGradientCards() {
if (options.gradient_cards === true) { if (options.gradient_cards === true) {
let cardheads = document.querySelectorAll('.ic-DashboardCard__header_hero'); let cardheads = document.querySelectorAll('.ic-DashboardCard__header_hero');
// Only create + append the style once; never re-append an already- // Create the style once; re-appending triggers the MutationObserver and re-runs this function.
// attached element, since appending to <html> triggers the
// MutationObserver in checkDashboardReady() and re-runs this function.
let cardcss = document.querySelector("#gradientcss"); let cardcss = document.querySelector("#gradientcss");
if (!cardcss) { if (!cardcss) {
cardcss = document.createElement('style'); cardcss = document.createElement('style');
@ -4144,9 +4450,7 @@ function changeGradientCards() {
document.documentElement.appendChild(cardcss); document.documentElement.appendChild(cardcss);
} }
// Build the full CSS into a string first, then only touch the DOM // Build CSS into a string and only touch the DOM if it changed.
// if the content actually changed. This keeps #gradientcss from being
// cleared/rewritten on every observer tick.
let css = ""; let css = "";
for (let i = 0; i < cardheads.length; i++) { for (let i = 0; i < cardheads.length; i++) {
let colorone = cardheads[i].style.backgroundColor.split(','); let colorone = cardheads[i].style.backgroundColor.split(',');
@ -4323,7 +4627,12 @@ async function getData(url) {
} }
}); });
let data = await response.json(); let data = await response.json();
return data // Deep-clone via JSON to unwrap Firefox Xray objects so nested props are mutable.
try {
return JSON.parse(JSON.stringify(data));
} catch (_) {
return data;
}
} }

View File

@ -94,6 +94,7 @@ via innerHTML. Exposes window.renderMarkdown(text) -> html.
const out = []; const out = [];
const n = lines.length; const n = lines.length;
let i = 0; let i = 0;
let taskSeq = 0; // ordinal of rendered task items, stable across code-block stashing
const isToken = (s) => /^\u0000\d+\u0000$/.test(s); const isToken = (s) => /^\u0000\d+\u0000$/.test(s);
while (i < n) { while (i < n) {
@ -140,7 +141,7 @@ via innerHTML. Exposes window.renderMarkdown(text) -> html.
if (task) { if (task) {
const checked = /x/i.test(task[1]); const checked = /x/i.test(task[1]);
items.push( items.push(
'<li class="cr-task"><input type="checkbox" disabled' + '<li class="cr-task" data-cr-task="' + taskSeq++ + '"><input type="checkbox"' +
(checked ? " checked" : "") + "> " + inline(task[2]) (checked ? " checked" : "") + "> " + inline(task[2])
); );
} else { } else {

View File

@ -38,10 +38,7 @@ const syncedSubOptions = [
]; ];
const localSwitches = []; const localSwitches = [];
// Theme export categories. Only visual/appearance settings belong in a // Theme export only carries visual settings, never personal productivity data.
// shared theme; personal productivity features (reminders, dashboard notes,
// the card-assignments list and its display prefs, card grades, item counts)
// are intentionally excluded so exported themes never carry personal stuff.
const exportDarkSchedule = ["auto_dark", "auto_dark_start", "auto_dark_end", "device_dark"]; const exportDarkSchedule = ["auto_dark", "auto_dark_start", "auto_dark_end", "device_dark"];
const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"]; const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"];
const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "cardSpacing", "cardWidth", "cardHeight"]; const exportCardStyles = ["customCardStyles", "imageSize", "cardRoundness", "cardSpacing", "cardWidth", "cardHeight"];
@ -433,10 +430,7 @@ function toggleBetterSidebarSubOptions(betterSidebarOn) {
} }
} }
// When the Better Todo List is on, its own "Hide Recent Feedback" sub-option // Hide the standalone feedback toggle when Better Todo's own sub-option replaces it.
// (todo_hide_feedback) replaces the standalone "Hide recent feedback" toggle,
// so hide the big #hide_feedback toggle to avoid showing two controls for the
// same thing. Mirrors toggleBetterSidebarSubOptions.
function toggleBetterTodoSubOptions(betterTodoOn) { function toggleBetterTodoSubOptions(betterTodoOn) {
const hideFeedbackEl = document.getElementById("hide_feedback"); const hideFeedbackEl = document.getElementById("hide_feedback");
if (hideFeedbackEl) { if (hideFeedbackEl) {
@ -444,10 +438,7 @@ function toggleBetterTodoSubOptions(betterTodoOn) {
} }
} }
// Hide the sub-options of a big toggle when it is turned off. // Hide a toggle's sub-options when it's off; auto_dark only hides its time clocks.
// For gpa_calc / assignments_due / better_todo the whole .sub-options block is
// hidden. For auto_dark ("schedule dark mode") only the start/end time clocks
// (.timesets) are hidden, leaving the device-dark checkbox visible.
function toggleSubOptionsVisibility(option, isOn) { function toggleSubOptionsVisibility(option, isOn) {
const togglesWithSubOptions = ["gpa_calc", "assignments_due", "better_todo", "auto_dark"]; const togglesWithSubOptions = ["gpa_calc", "assignments_due", "better_todo", "auto_dark"];
if (!togglesWithSubOptions.includes(option)) return; if (!togglesWithSubOptions.includes(option)) return;
@ -480,9 +471,7 @@ function setupFeatureSearch(menu) {
function shouldSkip(el) { function shouldSkip(el) {
// Skip overlays / scrapped containers that aren't real features // Skip overlays / scrapped containers that aren't real features
if (el.closest('#submit-popup, #browser-settings-popup, #opt-in, #card-edit-menu')) return true; if (el.closest('#submit-popup, #browser-settings-popup, #opt-in, #card-edit-menu')) return true;
// Skip anything inside a statically-hidden option-container // Skip statically-hidden containers but keep dynamically-hidden sub-options.
// (e.g. the scrapped "remind" block, "Submit your theme").
// Dynamically-hidden sub-options (whose parent toggle is off) are kept.
let node = el; let node = el;
while (node && node !== document.body) { while (node && node !== document.body) {
if (node.classList && node.classList.contains('option-container') && node.style.display === 'none') return true; if (node.classList && node.classList.contains('option-container') && node.style.display === 'none') return true;
@ -604,9 +593,7 @@ function setupFeatureSearch(menu) {
// Home: sub-options / timesets / labelled rows (e.g. "Use dd/mm", "Start time", max-items) // Home: sub-options / timesets / labelled rows (e.g. "Use dd/mm", "Start time", max-items)
document.querySelectorAll(".options .sub-option, .options .timeset, .options .sub-options > div").forEach(sub => { document.querySelectorAll(".options .sub-option, .options .timeset, .options .sub-options > div").forEach(sub => {
if (shouldSkip(sub)) return; if (shouldSkip(sub)) return;
// Skip statically-hidden sub-options (e.g. the "hover preview" TODO). // Skip statically-hidden sub-options; dynamically-hidden ones stay indexed.
// Dynamically-hidden sub-options have display:none on their parent
// .sub-options, not on themselves, so they stay indexed.
if (sub.classList.contains("sub-option") && sub.style.display === "none") return; if (sub.classList.contains("sub-option") && sub.style.display === "none") return;
const text = labelOf(sub); const text = labelOf(sub);
if (!text) return; if (!text) return;
@ -1123,6 +1110,21 @@ function setup() {
}); });
}); });
// Toggle all export checkboxes and dispatch one change event to refresh output.
const exportCheckboxes = () => document.querySelectorAll(".export-details input");
const refreshExport = () => {
const first = exportCheckboxes()[0];
if (first) first.dispatchEvent(new Event("change", { bubbles: true }));
};
document.querySelector("#export-check-all")?.addEventListener("click", () => {
exportCheckboxes().forEach(c => { c.checked = true; });
refreshExport();
});
document.querySelector("#export-check-none")?.addEventListener("click", () => {
exportCheckboxes().forEach(c => { c.checked = false; });
refreshExport();
});
// activate revert to original button // activate revert to original button
document.querySelector("#theme-revert").addEventListener("click", () => { document.querySelector("#theme-revert").addEventListener("click", () => {
chrome.storage.local.get("previous_theme", local => { chrome.storage.local.get("previous_theme", local => {
@ -1575,9 +1577,7 @@ function saveCurrentTheme() {
function displayThemeList(direction = 0) { function displayThemeList(direction = 0) {
// render the full sorted theme list (filtered by any active search). // Render the sorted theme list, filtered by any active search.
// Previously this was an empty stub, so the browser showed nothing
// until the search box fired displayThemeSearchList directly.
let themesToShow = allThemes; let themesToShow = allThemes;
if (searchFor) { if (searchFor) {
themesToShow = allThemes.filter(theme => theme.title.toLowerCase().includes(searchFor.toLowerCase())); themesToShow = allThemes.filter(theme => theme.title.toLowerCase().includes(searchFor.toLowerCase()));

View File

@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Canvas Refined", "name": "Canvas Refined",
"description": "Even More Feature packed extension for Canvas.", "description": "Even More Feature packed extension for Canvas.",
"version": "6.3.0", "version": "6.3.1",
"icons": { "icons": {
"16": "icon/icon-16.png", "16": "icon/icon-16.png",
"32": "icon/icon-32.png", "32": "icon/icon-32.png",