diff --git a/css/content.css b/css/content.css index be25f89..2967250 100644 --- a/css/content.css +++ b/css/content.css @@ -268,16 +268,12 @@ .canvasrefined-quiz-safe-dismiss:hover { background: #ececec; } /* ===== Global Canvas Search ===== */ -.canvasrefined-gs-fab { - position: fixed; right: 18px; bottom: 18px; z-index: 2147483000; - width: 46px; height: 46px; border-radius: 50%; border: none; cursor: pointer; - display: inline-flex; align-items: center; justify-content: center; - color: #fff; background: var(--bclinks, #0072b5); - box-shadow: 0 4px 14px rgba(0,0,0,.28); - transition: transform .12s ease, filter .12s ease; -} -.canvasrefined-gs-fab:hover { transform: translateY(-2px) scale(1.04); filter: brightness(1.08); } -.canvasrefined-gs-fab:active { transform: translateY(0) scale(0.97); } +/* Native global-nav search item — matches Canvas' own nav links. */ +.canvasrefined-gs-nav-item { list-style: none; } +.canvasrefined-gs-nav-item .ic-app-header__menu-list-link { cursor: pointer; } +.canvasrefined-gs-nav-item .menu-item-icon-container svg { width: 26px; height: 26px; } +.canvasrefined-gs-nav-item:hover .ic-app-header__menu-list-link { background: #0000004f !important; } +.canvasrefined-gs-nav-item .menu-item__text { font-size: 12px; } /* Dashboard header trigger — sits just left of the \u22ee options button. */ .canvasrefined-gs-header-btn { diff --git a/js/content.js b/js/content.js index 5c24828..962d3a3 100644 --- a/js/content.js +++ b/js/content.js @@ -5556,8 +5556,9 @@ function setupGlobalSearch() { } function removeGlobalSearch() { - document.getElementById("canvasrefined-global-search-btn")?.remove(); document.getElementById("canvasrefined-global-search-header-btn")?.remove(); + removeGlobalSearchBetterSidebarButton(); + removeGlobalSearchNativeSidebarButton(); closeGlobalSearchModal(); if (_gsPlacementObserver) { _gsPlacementObserver.disconnect(); _gsPlacementObserver = null; } if (_gsShortcutBound) { @@ -5566,10 +5567,14 @@ function removeGlobalSearch() { } } -// Placement: on the dashboard the trigger lives in the header (between the -// "Dashboard" heading and the options menu, right-aligned). Everywhere else it -// falls back to a floating action button. A rAF-debounced MutationObserver -// re-evaluates placement as Canvas renders/SPA-navigates. +// Shared search icon used by the sidebar + header triggers. +const GLOBAL_SEARCH_ICON_SVG = ``; + +// Placement: a search trigger is injected into whichever left sidebar is +// active — the Better Sidebar (when enabled) or Canvas' native global nav — +// and, on the dashboard, a button is also placed in the header actions row. +// There is no floating button. A rAF-debounced MutationObserver re-evaluates +// placement as Canvas renders/SPA-navigates/rebuilds the sidebar. let _gsPlacementObserver = null; let _gsPlacementScheduled = false; function ensureGlobalSearchButton() { @@ -5588,36 +5593,98 @@ function ensureGlobalSearchButton() { function placeGlobalSearchTrigger() { if (options.global_search !== true) return; + + // Native global nav (the slim icon bar) — always present, so always add. + ensureGlobalSearchNativeSidebarButton(); + + // Better Sidebar (extra column when the option is enabled) — add when present. + const betterSidebar = document.getElementById("better-sidebar-container"); + if (betterSidebar) { + ensureGlobalSearchBetterSidebarButton(betterSidebar); + } else { + removeGlobalSearchBetterSidebarButton(); + } + + // Dashboard header button (in addition to the sidebar triggers). const headerActions = document.querySelector(".ic-Dashboard-header__actions"); if (isDashboardPage() && headerActions) { ensureGlobalSearchHeaderButton(headerActions); - document.getElementById("canvasrefined-global-search-btn")?.remove(); } else { document.getElementById("canvasrefined-global-search-header-btn")?.remove(); - ensureGlobalSearchFab(); } } -function ensureGlobalSearchFab() { - if (document.getElementById("canvasrefined-global-search-btn")) return; - const btn = document.createElement("button"); - btn.id = "canvasrefined-global-search-btn"; - btn.type = "button"; - btn.className = "canvasrefined-gs-fab"; +// --- Better Sidebar trigger -------------------------------------------------- + +function ensureGlobalSearchBetterSidebarButton(betterSidebar) { + // The first child of #better-sidebar-container is the button list. + const sidebarContent = betterSidebar.querySelector("div"); + if (!sidebarContent) return; + if (sidebarContent.querySelector("#canvasrefined-gs-sidebar-btn")) return; + const btn = document.createElement("a"); + btn.id = "canvasrefined-gs-sidebar-btn"; + btn.className = "canvasrefined-custom-btn better-sidebar-btn canvasrefined-gs-sidebar-btn"; + btn.href = "#"; btn.title = "Search Canvas (Ctrl+K)"; + btn.setAttribute("role", "button"); btn.setAttribute("aria-label", "Search Canvas"); - btn.innerHTML = ``; - btn.addEventListener("click", openGlobalSearchModal); - if (document.body) { - document.body.appendChild(btn); - } else { - const wait = new MutationObserver(() => { - if (document.body) { document.body.appendChild(btn); wait.disconnect(); } - }); - wait.observe(document.documentElement, { childList: true }); - } + btn.style.cssText = "width:40%;height:var(--bc-sidebar-btn-height,30px);cursor:pointer;text-align:center;text-decoration:none;display:inline-flex;justify-content:center;align-items:center;gap:var(--bc-sidebar-btn-gap,8px);color:var(--bcsidebar-text) !important;font-weight:bold;position:relative;"; + btn.innerHTML = `${GLOBAL_SEARCH_ICON_SVG}Search`; + btn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); openGlobalSearchModal(); }); + // Append so it sits at the bottom of the sidebar item list. + sidebarContent.appendChild(btn); + // Match the sidebar's current expanded/collapsed mode immediately so the + // button doesn't briefly render in the wrong state (e.g. label visible while + // collapsed) until the next toggle calls updateSidebar(). + applyGlobalSearchSidebarButtonMode(btn, betterSidebar.dataset.expanded === "true"); } +function removeGlobalSearchBetterSidebarButton() { + document.getElementById("canvasrefined-gs-sidebar-btn")?.remove(); +} + +// Apply the Better Sidebar's current expanded/collapsed styling to the search +// button, mirroring updateSidebar()'s button/label/svg rules so the button is +// correct the moment it's inserted (and whenever the sidebar re-renders). +function applyGlobalSearchSidebarButtonMode(btn, expanded) { + if (!btn) return; + btn.style.width = expanded ? "80%" : "40%"; + const label = btn.querySelector(".better-sidebar-label"); + if (label) label.style.display = expanded ? "block" : "none"; + btn.querySelectorAll("svg").forEach(svg => { + svg.style.width = "var(--bc-sidebar-icon-size,20px)"; + svg.style.height = "var(--bc-sidebar-icon-size,20px)"; + }); +} + +// --- Native global-nav trigger ---------------------------------------------- + +function ensureGlobalSearchNativeSidebarButton() { + const navMenu = document.getElementById("menu"); + if (!navMenu) return; + if (navMenu.querySelector("#canvasrefined-gs-nav-item")) return; + const li = document.createElement("li"); + li.id = "canvasrefined-gs-nav-item"; + li.className = "ic-app-header__menu-list-item canvasrefined-gs-nav-item"; + const link = document.createElement("a"); + link.className = "ic-app-header__menu-list-link"; + link.href = "#"; + link.setAttribute("role", "button"); + link.title = "Search Canvas (Ctrl+K)"; + link.setAttribute("aria-label", "Search Canvas"); + link.innerHTML = `Search`; + link.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); openGlobalSearchModal(); }); + li.appendChild(link); + // Append so the search item appears at the bottom of the global nav. + navMenu.appendChild(li); +} + +function removeGlobalSearchNativeSidebarButton() { + document.getElementById("canvasrefined-gs-nav-item")?.remove(); +} + +// --- Dashboard header trigger ----------------------------------------------- + function ensureGlobalSearchHeaderButton(headerActions) { if (headerActions.querySelector("#canvasrefined-global-search-header-btn")) return; const btn = document.createElement("button"); @@ -5642,14 +5709,19 @@ function ensureGlobalSearchShortcut() { function onGlobalSearchShortcut(e) { // Ctrl/Cmd+K toggles the search modal. Ignore when a modal is already open // and the user is typing in its input (handled by the modal's own listener). - if ((e.ctrlKey || e.metaKey) && (e.key === "k" || e.key === "K")) { - const modal = document.getElementById("canvasrefined-global-search-modal"); - if (modal && modal.dataset.open === "true") { - closeGlobalSearchModal(); - } else { - e.preventDefault(); - openGlobalSearchModal(); - } + if (!(e.ctrlKey || e.metaKey) || !(e.key === "k" || e.key === "K")) return; + + // Never activate on quiz pages (intro or take) so we don't interfere with + // the quiz experience or the browser's native Ctrl+K. Read the URL live + // because current_page can be stale after Canvas' client-side navigation. + if (/^\/courses\/\d+\/quizzes\/\d+(?:\/|$)/.test(window.location.pathname)) return; + + const modal = document.getElementById("canvasrefined-global-search-modal"); + if (modal && modal.dataset.open === "true") { + closeGlobalSearchModal(); + } else { + e.preventDefault(); + openGlobalSearchModal(); } } @@ -5719,11 +5791,6 @@ function openGlobalSearchModal() { }), 120); }); - // Block the page scroll while the modal is open. - const prevOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; - modal._restoreOverflow = () => { document.body.style.overflow = prevOverflow; }; - requestAnimationFrame(() => input.focus()); // Kick off indexing immediately so the first keystroke is fast. ensureGlobalSearchIndex(); @@ -5734,7 +5801,6 @@ function openGlobalSearchModal() { function closeGlobalSearchModal() { const modal = document.getElementById("canvasrefined-global-search-modal"); if (!modal) return; - if (typeof modal._restoreOverflow === "function") modal._restoreOverflow(); modal.remove(); }