diff --git a/css/content.css b/css/content.css index fa4f8be..94dfaf2 100644 --- a/css/content.css +++ b/css/content.css @@ -354,7 +354,8 @@ .canvasrefined-gs-type-module { background: #efe7fb; color: #5b2a9e; border-color: #d8c6f1; } .canvasrefined-gs-type-page { background: #f1f3f5; color: #444; border-color: #dde1e3; } .canvasrefined-gs-type-file { background: #f1f3f5; color: #444; border-color: #dde1e3; } -.canvasrefined-gs-type-link, .canvasrefined-gs-type-external-tool { background: #fff4d6; color: #7a5b00; border-color: #f1dfab; } + .canvasrefined-gs-type-link, .canvasrefined-gs-type-external-tool { background: #fff4d6; color: #7a5b00; border-color: #f1dfab; } +.canvasrefined-gs-type-course { background: #dff3f3; color: #0b6e6e; border-color: #b3dede; } .canvasrefined-gs-footer { display: flex; flex-wrap: wrap; gap: 12px; padding: 8px 14px; font-size: 12px; color: var(--bctext-2, #9aa3ab); border-top: 1px solid var(--bcborders, #e2e6e8); diff --git a/js/content.js b/js/content.js index f8078e1..8e60f35 100644 --- a/js/content.js +++ b/js/content.js @@ -6796,7 +6796,7 @@ function openGlobalSearchModal() { `; +} +function bindGlobalSearchRows(resultsEl, top) { resultsEl.querySelectorAll(".canvasrefined-gs-row").forEach((row) => { // Plain click / Ctrl+click: honor modifier for new-tab behavior. row.addEventListener("click", (e) => { @@ -7168,6 +7186,74 @@ async function runGlobalSearch(query, resultsEl) { if (item) openGlobalSearchResult(item, true); }); }); +} + +async function runGlobalSearch(query, resultsEl) { + if (!globalSearchIndex && globalSearchIndexPromise) { + resultsEl.innerHTML = `
Building search index\u2026
`; + } + const index = await ensureGlobalSearchIndex(); + if (!query) { + resultsEl.innerHTML = `
Start typing to search your modules and assignments. Use @coursename to search a specific course.
`; + return []; + } + if (!index || !index.length) { + resultsEl.innerHTML = `
No modules or assignments found. Open the search again later if your courses are still loading.
`; + return []; + } + + // "@course name" narrows the search to one course. With no other words + // after the filter (or a bare "@"), list the matching courses' homepages. + const courseEntries = index.filter(i => i.type === "Course"); + const parsed = parseGlobalSearchCourseFilter(query, courseEntries); + let effectiveQuery = query; + let allowedCourseIds = null; + if (parsed) { + effectiveQuery = parsed.content; + const matching = courseEntries.filter(c => globalSearchCourseMatches(c, parsed.courseFilter)); + allowedCourseIds = new Set(matching.map(c => String(c.courseId))); + if (!effectiveQuery) { + if (!matching.length) { + resultsEl.innerHTML = `
No course matches \u201c${escapeGlobalSearchHtml(parsed.courseFilter)}\u201d.
`; + return []; + } + resultsEl.innerHTML = matching.map((item, i) => globalSearchRowHtml(item, i)).join(""); + bindGlobalSearchRows(resultsEl, matching); + return matching; + } + } + + const q = effectiveQuery.toLowerCase(); + const matches = []; + for (const item of index) { + // Re-check hidden status at search time so a card hidden after the index + // was cached (10-min TTL) never surfaces in results. + if (isCourseHidden(item.courseId)) continue; + // An @course filter restricts results to that course's entries. + if (allowedCourseIds && !allowedCourseIds.has(String(item.courseId))) continue; + const t = (item.title || "").toLowerCase(); + const c = (item.course || "").toLowerCase(); + let score = -1; + if (t.startsWith(q)) score = 100 - t.indexOf(q); + else if (t.includes(q)) score = 60 - t.indexOf(q); + else if (c.includes(q)) score = 20; + // Course homepages float above item matches for class-name queries. + if (score >= 0) { + if (item.type === "Course" && t.startsWith(q)) score += 40; + item._score = score + (t === q ? 50 : 0); + matches.push(item); + } + } + matches.sort((a, b) => b._score - a._score); + const top = matches.slice(0, 50); + + if (!top.length) { + resultsEl.innerHTML = `
No results for \u201c${escapeGlobalSearchHtml(query)}\u201d.
`; + return []; + } + + resultsEl.innerHTML = top.map((item, i) => globalSearchRowHtml(item, i)).join(""); + bindGlobalSearchRows(resultsEl, top); return top; }