|
| 1 | +// Pure breadcrumb model: maps the current route to the trail rendered under |
| 2 | +// the top menu. Each crumb is { label, href } for navigable ancestors, |
| 3 | +// { label, current: true } for the page being viewed, or a plain { label } |
| 4 | +// when the only sensible link target would be the page itself (crumbs never |
| 5 | +// self-link). Routes that are not a user listing or repository report get no |
| 6 | +// trail (empty array), so landing and static content pages stay crumb-free. |
| 7 | +import { classifyRoute, buildReportUrl, getQueryParam } from './routes.js'; |
| 8 | + |
| 9 | +// CustomEvent dispatched by ReportView once the report fetch resolves the |
| 10 | +// branch actually loaded (a `main` 404 falls back to `master`). Breadcrumbs |
| 11 | +// listens and relabels the current crumb so it identifies the report being |
| 12 | +// displayed instead of the default guess. |
| 13 | +export const BRANCH_RESOLVED_EVENT = 'rf:branch-resolved'; |
| 14 | + |
| 15 | +/** |
| 16 | + * Creates a branch-resolution event in the dispatch target's realm. |
| 17 | + * Falls back to an Event carrying detail when CustomEvent is unavailable or |
| 18 | + * incompatible with Event in the current runtime. |
| 19 | + * |
| 20 | + * @param {EventTarget} target - Window, DOM node, or standalone event target. |
| 21 | + * @param {{username: string, repository: string, branch: string}} detail |
| 22 | + * The report identity and resolved branch carried by the event. |
| 23 | + * @returns {Event & {detail: object}} An event compatible with the target. |
| 24 | + */ |
| 25 | +function branchResolvedEvent(target, detail) { |
| 26 | + const realm = |
| 27 | + (target && target.window === target && target) || // a Window |
| 28 | + target?.ownerDocument?.defaultView || // a DOM Node |
| 29 | + null; |
| 30 | + if (realm && typeof realm.CustomEvent === 'function') { |
| 31 | + return new realm.CustomEvent(BRANCH_RESOLVED_EVENT, { detail }); |
| 32 | + } |
| 33 | + if (typeof CustomEvent === 'function' && new CustomEvent('') instanceof Event) { |
| 34 | + return new CustomEvent(BRANCH_RESOLVED_EVENT, { detail }); |
| 35 | + } |
| 36 | + return Object.assign(new Event(BRANCH_RESOLVED_EVENT), { detail }); |
| 37 | +} |
| 38 | + |
| 39 | +/** |
| 40 | + * Builds a user-listing path with the username encoded as one URL segment. |
| 41 | + * |
| 42 | + * @param {string} username - Repository owner's username. |
| 43 | + * @returns {string} The user-listing path, without a deployment base path. |
| 44 | + */ |
| 45 | +function userHref(username) { |
| 46 | + return `/${encodeURIComponent(username)}`; |
| 47 | +} |
| 48 | + |
| 49 | +/** |
| 50 | + * Announces the branch a report fetch actually resolved to. |
| 51 | + * |
| 52 | + * @param {EventTarget} target - Dispatch target (window in the browser). |
| 53 | + * @param {{username: string, repository: string, branch: string}} report |
| 54 | + * The report identity plus its resolved branch. |
| 55 | + */ |
| 56 | +export function announceResolvedBranch(target, { username, repository, branch }) { |
| 57 | + target.dispatchEvent( |
| 58 | + branchResolvedEvent(target, { username, repository, branch }) |
| 59 | + ); |
| 60 | +} |
| 61 | + |
| 62 | +/** |
| 63 | + * Checks whether a branch-resolution announcement belongs to the report |
| 64 | + * route currently being viewed (stale announcements from a previous page |
| 65 | + * must not relabel it). |
| 66 | + * |
| 67 | + * @param {string} path - Current pathname. |
| 68 | + * @param {{username?: string, repository?: string}} detail - Event detail. |
| 69 | + * @returns {boolean} True when the announcement matches the viewed report. |
| 70 | + */ |
| 71 | +export function reportMatchesFor(path, detail = {}) { |
| 72 | + const route = classifyRoute(path); |
| 73 | + return route.type === 'report' |
| 74 | + && route.username === detail.username |
| 75 | + && route.repository === detail.repository; |
| 76 | +} |
| 77 | + |
| 78 | +/** |
| 79 | + * Builds the breadcrumb trail for a path. |
| 80 | + * |
| 81 | + * @param {string} path - Current pathname (basePath already stripped by Next). |
| 82 | + * @param {string} [search] - Query string conveying ?branch= on deep links. |
| 83 | + * @param {string|null} [resolvedBranch] - Branch the report actually loaded |
| 84 | + * from (announced by ReportView after the fetch; null beforehand). |
| 85 | + * @returns {Array<{label: string, href?: string, current?: boolean}>} Crumbs |
| 86 | + * in hierarchical order; the last crumb is the current page. |
| 87 | + */ |
| 88 | +export function breadcrumbsFor(path, search = '', resolvedBranch = null) { |
| 89 | + const route = classifyRoute(path); |
| 90 | + |
| 91 | + if (route.type === 'user') { |
| 92 | + return [ |
| 93 | + { label: 'Home', href: '/' }, |
| 94 | + { label: route.username, current: true } |
| 95 | + ]; |
| 96 | + } |
| 97 | + |
| 98 | + if (route.type === 'report') { |
| 99 | + const hasBranchSegment = path.split('/').filter(Boolean).length > 2; |
| 100 | + const explicitBranch = hasBranchSegment |
| 101 | + ? route.branch |
| 102 | + : getQueryParam(search, 'branch'); |
| 103 | + // The resolved branch outranks any explicit/requested branch: when main |
| 104 | + // 404s and the fallback loads master, the label must say "master". |
| 105 | + const branch = resolvedBranch || explicitBranch || null; |
| 106 | + |
| 107 | + const crumbs = [ |
| 108 | + { label: 'Home', href: '/' }, |
| 109 | + { label: route.username, href: userHref(route.username) } |
| 110 | + ]; |
| 111 | + |
| 112 | + if (!branch) { |
| 113 | + // Plain /user/repo before the report announces its branch: the |
| 114 | + // repository crumb IS the current page (linking it would self-link), |
| 115 | + // and the branch is not guessed — main may resolve to master. |
| 116 | + crumbs.push({ label: route.repository, current: true }); |
| 117 | + return crumbs; |
| 118 | + } |
| 119 | + |
| 120 | + // With an explicit branch (path segment or ?branch=), the repository |
| 121 | + // crumb navigates back up to the default-branch report. Without one the |
| 122 | + // only target would be the current page, so the crumb stays unlinked. |
| 123 | + crumbs.push( |
| 124 | + explicitBranch |
| 125 | + ? { label: route.repository, href: buildReportUrl(route.username, route.repository) } |
| 126 | + : { label: route.repository }, |
| 127 | + { label: branch, current: true } |
| 128 | + ); |
| 129 | + return crumbs; |
| 130 | + } |
| 131 | + |
| 132 | + return []; |
| 133 | +} |
0 commit comments