fixed return to assignment button

This commit is contained in:
Guy Sandler 2026-08-26 19:26:08 -07:00
parent 11bdfb0b3b
commit 97b2612062

View File

@ -81,29 +81,60 @@ function quizSafeModeActive() {
return isQuizPage() && options.quiz_safe_mode === true; return isQuizPage() && options.quiz_safe_mode === true;
} }
// Read the URL live: Canvas' "New Canvas" UI navigates client-side via
// history.pushState/replaceState, and current_page (captured at document_start)
// can be stale when the user clicks into a submission page. Reading
// window.location.pathname at check time makes this correct regardless.
function getSubmissionAssignmentLink() { function getSubmissionAssignmentLink() {
const match = current_page.match(/^\/courses\/(\d+)\/assignments\/(\d+)\/submissions\/(\d+)(?:\/|$)/); const match = window.location.pathname.match(/^\/courses\/(\d+)\/assignments\/(\d+)\/submissions\/(\d+)(?:\/|$)/);
if (!match) return null; if (!match) return null;
return `${domain}/courses/${match[1]}/assignments/${match[2]}/`; return `${domain}/courses/${match[1]}/assignments/${match[2]}/`;
} }
// The content container isn't always #content on every Canvas layout (submission
// pages in particular may render into .ic-Layout-contentMain or #main). Match the
// quiz-safe-mode banner's container finder so the button lands in the visible
// content area; fall back to <body> so injection never silently no-ops.
function findContentContainer() {
return document.querySelector(".ic-Layout-contentMain")
|| document.getElementById("content")
|| document.querySelector("#main")
|| document.body;
}
let submissionPageButtonObserver = null; let submissionPageButtonObserver = null;
let submissionButtonScheduled = false;
let profileLogoutButtonObserver = null; let profileLogoutButtonObserver = null;
let newCanvasButtonObserver = null; let newCanvasButtonObserver = null;
function addSubmissionPageButton() { function addSubmissionPageButton() {
const assignmentLink = getSubmissionAssignmentLink(); const assignmentLink = getSubmissionAssignmentLink();
if (!assignmentLink) return; if (!assignmentLink) return;
const content = document.getElementById("content"); // Place the button inline with the "Submission Details" heading and the
if (!content || content.querySelector("#canvasrefined-assignment-return")) return; // grade-values table, inside the .submission-details-header__heading-and-grades
// flex row (appended so it sits to the right of the grade summary). Only inject
// once that row exists; if it's not there yet the persistent MutationObserver
// re-tries on the next DOM change so we never fall back to body/#content
// (which would put the button at the bottom of the page).
const row = document.querySelector(".submission-details-header__heading-and-grades")
|| document.querySelector(".submission-details-header")
|| document.querySelector(".submission_details");
if (!row || row.querySelector("#canvasrefined-assignment-return")) return;
makeElement("a", content, { // Insert between the h1 heading and the grade-summary div so it reads
// [Heading] [Back to Assignment] [Grade]. Falls back to appending if the
// grade-summary div isn't found for some reason.
const gradeSummary = row.querySelector(".submission-details-header__grade-summary");
const btn = makeElement("a", row, {
id: "canvasrefined-assignment-return", id: "canvasrefined-assignment-return",
className: "canvasrefined-custom-btn", className: "canvasrefined-custom-btn",
href: assignmentLink, href: assignmentLink,
textContent: "Back to Assignment", textContent: "Back to Assignment",
style: "display:inline-flex;align-items:center;justify-content:center;align-self:flex-start;margin:0 0 12px 0;padding:10px 14px;text-decoration:none;font-weight:700;", style: "display:inline-flex;align-items:center;justify-content:center;align-self:center;margin-left:auto;margin-right:12px;padding:6px 12px;text-decoration:none;font-weight:700;color:inherit!important;",
}, true); });
if (gradeSummary && gradeSummary.parentNode === row) {
row.insertBefore(btn, gradeSummary);
}
} }
function addProfileLogoutPageButton() { function addProfileLogoutPageButton() {
@ -153,19 +184,26 @@ function watchProfileLogoutPageButton() {
}, 10000); }, 10000);
} }
function ensureSubmissionPageButton() { // Reconcile the button against the current page on a rAF-throttled schedule.
const assignmentLink = getSubmissionAssignmentLink(); // Canvas (React-based "New Canvas" UI) re-renders the content area on SPA
if (!assignmentLink) return false; // navigation and can wipe our injected node; a persistent observer re-adds it.
const content = document.getElementById("content"); function maintainSubmissionPageButton() {
if (!content) return false; if (submissionButtonScheduled) return;
const existing = content.querySelector("#canvasrefined-assignment-return"); submissionButtonScheduled = true;
if (existing) { requestAnimationFrame(() => {
// Keep the href in sync when navigating between submission pages via SPA routing. submissionButtonScheduled = false;
existing.href = assignmentLink; const link = getSubmissionAssignmentLink();
return true; const existing = document.getElementById("canvasrefined-assignment-return");
} if (!link) {
addSubmissionPageButton(); existing?.remove();
return Boolean(content.querySelector("#canvasrefined-assignment-return")); return;
}
if (existing) {
if (existing.href !== link) existing.href = link;
return;
}
addSubmissionPageButton();
});
} }
function isAssignmentPage() { function isAssignmentPage() {
@ -201,29 +239,19 @@ function watchSequenceFooter() {
}, 10000); }, 10000);
} }
// One persistent, rAF-throttled observer that keeps the button present whenever
// we're on a submission page. Unlike the old 10s-disconnecting observer, this
// survives Canvas' post-navigation re-renders that remove injected nodes. The
// extra delayed checks cover React hydration that wipes the button after our
// first add without emitting any later mutation for the observer to catch.
function watchSubmissionPageButton() { function watchSubmissionPageButton() {
if (!getSubmissionAssignmentLink()) {
// Navigated away from a submission page; remove any stale button left in #content.
document.getElementById("canvasrefined-assignment-return")?.remove();
return;
}
if (ensureSubmissionPageButton()) return;
if (submissionPageButtonObserver) return; if (submissionPageButtonObserver) return;
maintainSubmissionPageButton();
submissionPageButtonObserver = new MutationObserver(() => { submissionPageButtonObserver = new MutationObserver(maintainSubmissionPageButton);
if (ensureSubmissionPageButton() && submissionPageButtonObserver) {
submissionPageButtonObserver.disconnect();
submissionPageButtonObserver = null;
}
});
submissionPageButtonObserver.observe(document.documentElement, { childList: true, subtree: true }); submissionPageButtonObserver.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => { for (const ms of [300, 800, 1600, 3000, 5000]) {
if (submissionPageButtonObserver) { setTimeout(maintainSubmissionPageButton, ms);
submissionPageButtonObserver.disconnect(); }
submissionPageButtonObserver = null;
}
}, 10000);
} }
function removeNewCanvasButton() { function removeNewCanvasButton() {
@ -619,6 +647,15 @@ function startExtension() {
}); });
footerObserver.observe(document.documentElement, { childList: true, subtree: true }); footerObserver.observe(document.documentElement, { childList: true, subtree: true });
// Start the submission-page "Back to Assignment" button watcher and the SPA
// navigation hook immediately, before the async storage callbacks below.
// These don't depend on `options`, and running them first means a throw in
// any later init step can't prevent the button from appearing. The watcher
// reads window.location.pathname live and uses a persistent MutationObserver
// to (re)inject the button once the content container exists.
setupNavigationListener();
watchSubmissionPageButton();
toggleDarkMode(); toggleDarkMode();
// Include bg_opacity/bg_blur so setupBetterSidebar (called below) tints the // Include bg_opacity/bg_blur so setupBetterSidebar (called below) tints the
@ -644,9 +681,7 @@ function startExtension() {
applyCustomBackground(); applyCustomBackground();
ensureBetterSidebar(); ensureBetterSidebar();
watchSequenceFooter(); watchSequenceFooter();
watchSubmissionPageButton();
watchProfileLogoutPageButton(); watchProfileLogoutPageButton();
setupNavigationListener();
setupQuizSafeModeBanner(); setupQuizSafeModeBanner();