const domain = window.location.origin; let current_page = window.location.pathname; // Canvas' "New Canvas" UI navigates client-side via history.pushState/ // replaceState without a full page reload. current_page is captured once at // document_start, so without this hook it goes stale and page-specific features // (Back to Assignment button, sequence-footer removal, profile logout button) // never activate when the user clicks into a page instead of loading it directly. function setupNavigationListener() { const update = () => { const next = window.location.pathname; if (next === current_page) return; current_page = next; // Re-run the page-scoped watchers so they evaluate against the new URL. watchSequenceFooter(); watchSubmissionPageButton(); watchProfileLogoutPageButton(); watchGradeAnalytics(); }; for (const method of ["pushState", "replaceState"]) { const orig = history[method]; history[method] = function (...args) { const ret = orig.apply(this, args); update(); return ret; }; } window.addEventListener("popstate", update); } function getCurrentCourseId() { const match = current_page.match(/^\/courses\/(\d+)(?:\/|$)/); return match ? parseInt(match[1]) : null; } function getSidebarLayoutMode() { if (current_page.match(/^\/courses\/(\d+)(?:\/|$)/)) return "course"; if (isProfilePage()) return "course"; if (current_page === "/courses" || current_page === "/courses/") return "dash"; if (current_page === "/" || current_page === "") return "dash"; return "dash"; } function isGradesPage() { return /^\/courses\/\d+\/grades(?:\/|$)/.test(current_page); } function isCoursesIndexPage() { return /^\/courses\/?$/.test(current_page); } function isGroupsIndexPage() { return /^\/groups\/?$/.test(current_page); } function isConversationsPage() { return /^\/conversations(?:\/|$)/.test(current_page); } function isAccountsPage() { return /^\/accounts(?:\/|$)/.test(current_page); } function isProfilePage() { return /^\/profile(?:\/|$)/.test(current_page); } // Quiz pages: /courses/123/quizzes/456 (pre-take/intro) and // /courses/123/quizzes/456/take (the actual quiz). function isQuizPage() { return /^\/courses\/\d+\/quizzes\/\d+(?:\/|$)/.test(current_page); } function isQuizTakePage() { return /^\/courses\/\d+\/quizzes\/\d+\/take(?:\/|$)/.test(current_page); } function isQuizPreTakePage() { return isQuizPage() && !isQuizTakePage(); } // "Quiz safe mode" disables features that interfere with the default Canvas // quiz experience. Only active on quiz pages when the user has opted in. function quizSafeModeActive() { 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() { const match = window.location.pathname.match(/^\/courses\/(\d+)\/assignments\/(\d+)\/submissions\/(\d+)(?:\/|$)/); if (!match) return null; 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
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 submissionButtonScheduled = false; let assignmentButtonScheduled = false; let profileLogoutButtonObserver = null; let newCanvasButtonObserver = null; let sequenceFooterObserver = null; // Current user id, needed to build "Go to Grades" links on assignment pages. // The page's ENV global isn't visible to content scripts (isolated world), so // ask the Canvas API once and cache the result. // undefined = not fetched yet, null = fetch failed, number = ok. let currentUserIdCache; let currentUserIdPromise = null; function ensureCurrentUserId() { if (currentUserIdCache !== undefined) return Promise.resolve(currentUserIdCache); if (!currentUserIdPromise) { currentUserIdPromise = getData(`${domain}/api/v1/users/self`) .then(user => { currentUserIdCache = (user && user.id) || null; }) .catch(() => { currentUserIdCache = null; }) .then(() => { currentUserIdPromise = null; return currentUserIdCache; }); } return currentUserIdPromise; } // Assignment pages (/courses/123/assignments/456) link to the current user's // submission ("grades") page for that assignment. The lookahead keeps this // from matching the submission pages themselves (/.../submissions/678). function getAssignmentGradesLink() { const match = window.location.pathname.match(/^\/courses\/(\d+)\/assignments\/(\d+)(?!\/submissions)(?:\/|$)/); if (!match || currentUserIdCache == null) return null; return `${domain}/courses/${match[1]}/assignments/${match[2]}/submissions/${currentUserIdCache}`; } function addSubmissionPageButton() { const assignmentLink = getSubmissionAssignmentLink(); if (!assignmentLink) return; // Place the button inline with the "Submission Details" heading and the // 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; // 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", className: "canvasrefined-custom-btn", href: assignmentLink, textContent: "Back to Assignment", 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;", }); if (gradeSummary && gradeSummary.parentNode === row) { row.insertBefore(btn, gradeSummary); } } // Assignment pages: /courses/123/assignments/456 — add a "Go to Grades" button // to the right edge of the title row. function addAssignmentPageButton() { const link = getAssignmentGradesLink(); if (!link) return; // Place the button inside the assignment header's .title-content block, // pinned to its right edge on the title's line. .title-content is a plain // block wrapping the${c}`);
h = h.replace(/\*\*([^*]+?)\*\*/g, "$1");
h = h.replace(/~~([^~]+?)~~/g, "${q.join("`); 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(`
")}
${para.map(inline).join("
")}