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-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}
#better-todo-new-task-date,
#better-todo-new-task-time {
@ -58,10 +58,9 @@
font-size: 12px;
padding-inline: 6px;
}
#better-todo-new-task-date::-webkit-calendar-picker-indicator {
filter: invert(1) brightness(2);
opacity: .95;
}
/* The native date/time picker icons are themed in dark mode only via
color-scheme (see DARKMODE_CSS in css/darkmodecss.js); light mode keeps the
default dark glyphs on the light surface. */
.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
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 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 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 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;}

View File

@ -1193,4 +1193,18 @@ hr {
.event-details strong {
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>
<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 class="export-inputs">
<div class="sub-option">

View File

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

View File

@ -284,9 +284,7 @@ function createNasaInfoOverlay() {
const panel = nasaInfoOverlayEl.querySelector("#nasa-info-panel");
const populatePanel = async () => {
// The NASA worker (background.js getNasaBackground) falls back up to 7 days
// when today's APOD isn't available yet, so search backward for the date
// that was actually cached and displayed instead of assuming today.
// Search backward up to 7 days for the APOD actually cached, since today's may not be ready.
const date = new Date();
for (let i = 0; i < 7; i++) {
const dateStr = date.toISOString().slice(0, 10);
@ -620,8 +618,7 @@ function applyOptionsChanges(changes) {
loadCardAssignments();
break;
case "equal_height_cards":
// No need to rebuild the assignment rows — just stretch (or reset)
// the card heights in place for a snappy toggle.
// Stretch or reset card heights in place instead of rebuilding rows.
equalizeCardHeights();
break;
case "custom_cards":
@ -833,7 +830,7 @@ async function applyCustomBackground() {
-webkit-backdrop-filter: blur(8px) saturate(120%) !important;
}
#right-side-wrapper {
// backdrop-filter: blur(10px) !important;
/* backdrop-filter: blur(10px) !important; */
background-color: color-mix(in srgb, var(--bcbackground-0), transparent 35%);
border-radius: 5px;
}
@ -1553,6 +1550,7 @@ function buildPlannerNotePayload(form) {
const courseIdRaw = form.querySelector("#better-todo-new-task-course")?.value;
const dateValue = form.querySelector("#better-todo-new-task-date")?.value;
const timeValue = form.querySelector("#better-todo-new-task-time")?.value;
const link = form.querySelector("#better-todo-new-task-link")?.value?.trim() || "";
if (!title) {
throw new Error("Task title is required.");
@ -1570,6 +1568,7 @@ function buildPlannerNotePayload(form) {
return {
title,
details,
link,
courseId: courseIdRaw ? parseInt(courseIdRaw) : null,
// Canvas accepts local timestamp strings more reliably than UTC ISO strings for planner notes.
todoDate: `${dateValue}T${timeValue}:00`,
@ -1646,6 +1645,167 @@ async function createCanvasPlannerNote(payload) {
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) {
const cards = options.custom_cards || {};
const courseColors = options.custom_cards_3 || {};
@ -1705,7 +1865,8 @@ function ensureTodoTaskMenu(location, feedbackElement) {
});
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">
<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>
@ -1713,9 +1874,13 @@ function ensureTodoTaskMenu(location, feedbackElement) {
<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">
</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;">
<span id="better-todo-add-task-status" style="font-size:12px;color:var(--bctext-0);"></span>
<button id="better-todo-add-task-submit" class="canvasrefined-custom-btn" style="padding:4px 10px;cursor:pointer;" type="button">Create</button>
<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>
<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>
`;
@ -1729,32 +1894,90 @@ function ensureTodoTaskMenu(location, feedbackElement) {
courseSelect.addEventListener("change", () => updateTaskCourseSelectColor(courseSelect));
addTaskButton.addEventListener("click", () => {
const willOpen = !menu.classList.contains("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 submitButton = menu.querySelector("#better-todo-add-task-submit");
const deleteButton = menu.querySelector("#better-todo-add-task-delete");
status.textContent = "";
submitButton.disabled = true;
if (deleteButton) deleteButton.disabled = true;
try {
const payload = buildPlannerNotePayload(menu);
await createCanvasPlannerNote(payload);
status.textContent = "Task created.";
const editingId = menu.dataset.editingId || null;
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";
menu.querySelector("#better-todo-new-task-title").value = "";
menu.querySelector("#better-todo-new-task-details").value = "";
resetTaskFormToCreate(menu);
menu.classList.remove("canvasrefined-custom-open");
getAssignments();
clearTodoList();
createTodoSections(location);
} catch (e) {
status.textContent = e?.message || "Could not create task.";
status.textContent = e?.message || "Could not save task.";
status.style.color = "#db3754";
} finally {
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) {
if (!location.querySelector("#better-todo-header")) {
let header = makeElement("div", location, { id: "better-todo-header" });
@ -2071,6 +2352,10 @@ function populateAssignments(iscompleted = false) {
"#cccccc";
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 iconLeftOffset = isCustomTask ? 2 : 5;
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="display:flex;flex-direction:column;gap:3px;">
<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>
</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'">
<g id="SVGRepo_bgCarrier" stroke-width="0"></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");
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")) {
@ -3056,11 +3350,7 @@ async function changeColorPreset(colors) {
Dark mode
*/
// Light-mode fallback values for the --bc* theme variables. These are always
// 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.
// Light-mode fallbacks for the --bc* variables, always emitted so extension UI renders in light mode; dark mode overrides below.
const BC_LIGHT_DEFAULTS = {
"background-0": "#ffffff",
"background-1": "#c7c7c7",
@ -3264,12 +3554,8 @@ function createCardAssignment(assignment) {
let cardAssignments;
/*
Equal Height Cards when enabled, every dashboard card's assignment area is
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.
*/
/* Equal Height Cards: stretch each card's assignment area to match the tallest
one, using min-height so cards can still grow. */
let equalHeightResizeTimer = null;
function equalizeCardHeights() {
@ -3278,8 +3564,7 @@ function equalizeCardHeights() {
const enabled = options.equal_height_cards === true && options.assignments_due === true;
// Always clear any previously applied min-height first so we can either
// measure natural heights (when enabling) or fully reset (when disabling).
// Clear prior min-height so we can measure fresh or fully reset.
cards.forEach(card => {
const area = card.querySelector(".canvasrefined-card-assignment");
if (area) area.style.removeProperty("min-height");
@ -3287,9 +3572,7 @@ function equalizeCardHeights() {
if (!enabled) return;
// Measure the natural height of each card's assignment area, then stretch
// every area to the tallest one. Card headers are uniform, so equalizing
// the assignment area makes all cards the same total height.
// Stretch each assignment area to the tallest one.
let maxHeight = 0;
cards.forEach(card => {
const area = card.querySelector(".canvasrefined-card-assignment");
@ -3781,13 +4064,7 @@ function delayDashboardNotesStorage(text) {
}, 250);
}
/*
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.
*/
/* Fallback Markdown renderer used only if js/markdown.js failed to load. */
function crRenderMarkdownFallback(src) {
if (src == null) return "";
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 lines = text.split("\n");
let i = 0;
let taskSeq = 0; // ordinal of rendered task items, stable across code-block stashing
const inline = (t) => {
let h = escapeHtml(t);
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;
}
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)) {
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) {
if (!preview) return;
// Skip identical re-renders: writing innerHTML is a childList mutation that the
// 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.
// Skip identical re-renders to avoid a self-sustaining observer loop.
if (preview._crLastText === text) return;
preview._crLastText = text;
const renderer = (typeof window.renderMarkdown === "function") ? window.renderMarkdown : crRenderMarkdownFallback;
preview.innerHTML = renderer(text);
}
/*
Insert/wrap Markdown formatting in the notes editor at the current selection.
Dispatches a synthetic `input` event so the live preview + storage handler runs.
*/
/* Insert/wrap Markdown formatting at the selection and fire an input event. */
function notesApplyFormat(editor, action) {
if (!editor) return;
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 = `
<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>
@ -3990,7 +4290,18 @@ function wireDashboardNotes(notes) {
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);
editor.addEventListener("blur", exitEdit);
editor.addEventListener("input", function () {
@ -3998,11 +4309,10 @@ function wireDashboardNotes(notes) {
delayDashboardNotesStorage(this.value);
});
// Toolbar buttons: keep focus in the editor (mousedown preventDefault stops the
// button from stealing focus and collapsing back to the rendered view), then apply
// the formatting. Enters edit mode first if the user formats from the rendered view.
// preventDefault on the toolbar keeps focus in the editor across misclicks.
const toolbar = notes.querySelector(".canvasrefined-notes-toolbar");
if (toolbar) toolbar.addEventListener("mousedown", e => e.preventDefault());
notes.querySelectorAll(".cr-fmt").forEach(btn => {
btn.addEventListener("mousedown", e => e.preventDefault());
btn.addEventListener("click", () => {
if (!notes.classList.contains("is-editing")) enterEdit();
notesApplyFormat(editor, btn.dataset.action);
@ -4044,9 +4354,7 @@ function loadDashboardNotes() {
notes.style.display = "";
const editor = notes.querySelector(".canvasrefined-notes-editor");
const rendered = notes.querySelector(".canvasrefined-notes-rendered");
// While editing, the textarea is the source of truth: don't clobber it from
// 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.
// Only sync and render in view mode; the textarea is the source of truth while editing.
if (!notes.classList.contains("is-editing")) {
if (editor && editor.value !== (options.dashboard_notes_text || "")) {
editor.value = options.dashboard_notes_text || "";
@ -4134,9 +4442,7 @@ function changeGradientCards() {
if (options.gradient_cards === true) {
let cardheads = document.querySelectorAll('.ic-DashboardCard__header_hero');
// Only create + append the style once; never re-append an already-
// attached element, since appending to <html> triggers the
// MutationObserver in checkDashboardReady() and re-runs this function.
// Create the style once; re-appending triggers the MutationObserver and re-runs this function.
let cardcss = document.querySelector("#gradientcss");
if (!cardcss) {
cardcss = document.createElement('style');
@ -4144,9 +4450,7 @@ function changeGradientCards() {
document.documentElement.appendChild(cardcss);
}
// Build the full CSS into a string first, then only touch the DOM
// if the content actually changed. This keeps #gradientcss from being
// cleared/rewritten on every observer tick.
// Build CSS into a string and only touch the DOM if it changed.
let css = "";
for (let i = 0; i < cardheads.length; i++) {
let colorone = cardheads[i].style.backgroundColor.split(',');
@ -4323,7 +4627,12 @@ async function getData(url) {
}
});
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 n = lines.length;
let i = 0;
let taskSeq = 0; // ordinal of rendered task items, stable across code-block stashing
const isToken = (s) => /^\u0000\d+\u0000$/.test(s);
while (i < n) {
@ -140,7 +141,7 @@ via innerHTML. Exposes window.renderMarkdown(text) -> html.
if (task) {
const checked = /x/i.test(task[1]);
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])
);
} else {

View File

@ -38,10 +38,7 @@ const syncedSubOptions = [
];
const localSwitches = [];
// Theme export categories. Only visual/appearance settings belong in a
// 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.
// Theme export only carries visual settings, never personal productivity data.
const exportDarkSchedule = ["auto_dark", "auto_dark_start", "auto_dark_end", "device_dark"];
const exportCardColorToggles = ["gradient_cards", "disable_color_overlay"];
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
// (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.
// Hide the standalone feedback toggle when Better Todo's own sub-option replaces it.
function toggleBetterTodoSubOptions(betterTodoOn) {
const hideFeedbackEl = document.getElementById("hide_feedback");
if (hideFeedbackEl) {
@ -444,10 +438,7 @@ function toggleBetterTodoSubOptions(betterTodoOn) {
}
}
// Hide the sub-options of a big toggle when it is turned off.
// 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.
// Hide a toggle's sub-options when it's off; auto_dark only hides its time clocks.
function toggleSubOptionsVisibility(option, isOn) {
const togglesWithSubOptions = ["gpa_calc", "assignments_due", "better_todo", "auto_dark"];
if (!togglesWithSubOptions.includes(option)) return;
@ -480,9 +471,7 @@ function setupFeatureSearch(menu) {
function shouldSkip(el) {
// Skip overlays / scrapped containers that aren't real features
if (el.closest('#submit-popup, #browser-settings-popup, #opt-in, #card-edit-menu')) return true;
// Skip anything inside a statically-hidden option-container
// (e.g. the scrapped "remind" block, "Submit your theme").
// Dynamically-hidden sub-options (whose parent toggle is off) are kept.
// Skip statically-hidden containers but keep dynamically-hidden sub-options.
let node = el;
while (node && node !== document.body) {
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)
document.querySelectorAll(".options .sub-option, .options .timeset, .options .sub-options > div").forEach(sub => {
if (shouldSkip(sub)) return;
// Skip statically-hidden sub-options (e.g. the "hover preview" TODO).
// Dynamically-hidden sub-options have display:none on their parent
// .sub-options, not on themselves, so they stay indexed.
// Skip statically-hidden sub-options; dynamically-hidden ones stay indexed.
if (sub.classList.contains("sub-option") && sub.style.display === "none") return;
const text = labelOf(sub);
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
document.querySelector("#theme-revert").addEventListener("click", () => {
chrome.storage.local.get("previous_theme", local => {
@ -1575,9 +1577,7 @@ function saveCurrentTheme() {
function displayThemeList(direction = 0) {
// render the full 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.
// Render the sorted theme list, filtered by any active search.
let themesToShow = allThemes;
if (searchFor) {
themesToShow = allThemes.filter(theme => theme.title.toLowerCase().includes(searchFor.toLowerCase()));

View File

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