Skip to content

Commit bbd7133

Browse files
authored
fix: adapt repo page features to GitHub's new layout (#1004)
1 parent 2e360d5 commit bbd7133

File tree

14 files changed

+400
-116
lines changed

14 files changed

+400
-116
lines changed

src/helpers/get-github-repo-info.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ import * as pageDetect from 'github-url-detection';
55
import elementReady from 'element-ready';
66
import { getPlatform } from './get-platform';
77

8+
const repoSidebarSectionSelectors = [
9+
'.Layout-sidebar .BorderGrid-cell > .hide-sm.hide-md',
10+
'.Layout-sidebar .BorderGrid-cell .hide-sm.hide-md',
11+
'.BorderGrid-cell > .hide-sm.hide-md',
12+
'.BorderGrid-cell .hide-sm.hide-md',
13+
];
14+
15+
const repoSidebarMarkerSelector = [
16+
'a[href$="/stargazers"]',
17+
'a[href$="/watchers"]',
18+
'a[href$="/forks"]',
19+
'a[href$="/activity"]',
20+
'a[href*="/custom-properties"]',
21+
'a[href="#readme-ov-file"]',
22+
'.topic-tag.topic-tag-link',
23+
].join(', ');
24+
25+
const pickFirstVisible = <T extends HTMLElement>(elements: JQuery<T>) => {
26+
const $visibleElements = elements.filter(':visible');
27+
return ($visibleElements.length > 0 ? $visibleElements : elements).first();
28+
};
29+
830
export function getRepoName() {
931
const repoNameByUrl = getRepoNameByUrl();
1032
const repoNameByPage = getRepoNameByPage();
@@ -36,6 +58,27 @@ export function hasRepoContainerHeader() {
3658
return headerElement && !headerElement.attr('hidden');
3759
}
3860

61+
export function getRepoSidebarSection() {
62+
const $sections = $(repoSidebarSectionSelectors.join(','))
63+
.filter((_, element) => !element.closest('details-dialog, template'))
64+
.filter((_, element) => $(element).find(repoSidebarMarkerSelector).length > 0);
65+
66+
return pickFirstVisible($sections);
67+
}
68+
69+
export function getRepoSidebarBorderGrid() {
70+
const $sidebarSection = getRepoSidebarSection();
71+
if ($sidebarSection.length > 0) {
72+
return $sidebarSection.closest('.BorderGrid').first();
73+
}
74+
75+
const $borderGrids = $('.Layout-sidebar .BorderGrid, .BorderGrid')
76+
.filter((_, element) => !element.closest('details-dialog, template'))
77+
.filter((_, element) => $(element).find(repoSidebarMarkerSelector).length > 0);
78+
79+
return pickFirstVisible($borderGrids);
80+
}
81+
3982
export async function isRepoRoot() {
4083
return pageDetect.isRepoRoot();
4184
}

src/pages/ContentScripts/features/developer-networks/view.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ const View = ({ userName }: Props): JSX.Element => {
7474
>
7575
<span
7676
title={`${t('global_clickToshow')} ${t('component_developmentActivityNetwork_title')}`}
77-
className="Label"
77+
className="hypercrx-label"
7878
style={{
7979
color: 'var(--color-fg-default)',
8080
fontWeight: 'var(--base-text-weight-normal, 400)',
@@ -107,7 +107,7 @@ const View = ({ userName }: Props): JSX.Element => {
107107
>
108108
<span
109109
title={`${t('global_clickToshow')} ${t('component_openSourcePartnersNetwork_title')}`}
110-
className="Label"
110+
className="hypercrx-label"
111111
style={{
112112
color: 'var(--color-fg-default)',
113113
fontWeight: 'var(--base-text-weight-normal, 400)',
@@ -140,7 +140,7 @@ const View = ({ userName }: Props): JSX.Element => {
140140
>
141141
<span
142142
title={`${t('global_clickToshow')} ${t('component_openSourceInterestsNetwork_title')}`}
143-
className="Label"
143+
className="hypercrx-label"
144144
style={{
145145
color: 'var(--color-fg-default)',
146146
fontWeight: 'var(--base-text-weight-normal, 400)',

src/pages/ContentScripts/features/perceptor-tab/index.tsx

Lines changed: 144 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,98 @@ import sleep from '../../../../helpers/sleep';
99
import isGithub from '../../../../helpers/is-github';
1010

1111
const featureId = features.getFeatureID(import.meta.url);
12+
const insightsTabSelectors = [
13+
'nav[aria-label="Repository"] a[data-tab-item="insights"]',
14+
'a.UnderlineNav-item#insights-tab',
15+
].join(', ');
16+
let highlightingListenerAttached = false;
17+
let syncingPerceptorTab = false;
18+
let navigationObserver: MutationObserver | null = null;
19+
let observedRepositoryNavigation: HTMLElement | null = null;
20+
21+
const getInsightsTab = () => {
22+
const $tabs = $(insightsTabSelectors).filter((_, element) => !element.closest('template'));
23+
const $visibleTabs = $tabs.filter(':visible');
24+
25+
return ($visibleTabs.length > 0 ? $visibleTabs : $tabs).first();
26+
};
27+
28+
const getRepositoryNavigation = () => {
29+
const $navigations = $('nav[aria-label="Repository"]').filter((_, element) => !element.closest('template'));
30+
const $visibleNavigations = $navigations.filter(':visible');
31+
32+
return ($visibleNavigations.length > 0 ? $visibleNavigations : $navigations).first();
33+
};
34+
35+
const waitForMeasuredRepositoryNavigation = async (): Promise<HTMLElement | null> => {
36+
const repositoryNavigation = (await elementReady('nav[aria-label="Repository"]', {
37+
waitForChildren: false,
38+
})) as HTMLElement | null;
39+
40+
if (!repositoryNavigation) {
41+
return null;
42+
}
43+
44+
if (repositoryNavigation.dataset.overflowMeasured === 'true') {
45+
return repositoryNavigation;
46+
}
47+
48+
await new Promise<void>((resolve) => {
49+
let observer: MutationObserver;
50+
const timeout = window.setTimeout(() => {
51+
observer.disconnect();
52+
resolve();
53+
}, 1500);
54+
55+
observer = new MutationObserver(() => {
56+
if (repositoryNavigation.dataset.overflowMeasured === 'true') {
57+
window.clearTimeout(timeout);
58+
observer.disconnect();
59+
resolve();
60+
}
61+
});
62+
63+
observer.observe(repositoryNavigation, {
64+
attributes: true,
65+
attributeFilter: ['data-overflow-measured'],
66+
});
67+
});
68+
69+
return repositoryNavigation;
70+
};
1271

1372
const addPerceptorTab = async (): Promise<void | false> => {
1473
// the creation of the Perceptor tab is based on the Insights tab
15-
const insightsTab = await elementReady('a.UnderlineNav-item[id="insights-tab"]', { waitForChildren: false });
74+
await waitForMeasuredRepositoryNavigation();
75+
await elementReady(insightsTabSelectors, { waitForChildren: false });
76+
const $insightsTab = getInsightsTab();
77+
const insightsTab = $insightsTab[0] as HTMLAnchorElement | undefined;
1678
if (!insightsTab) {
17-
// if the selector failed to find the Insights tab
1879
return false;
1980
}
20-
const perceptorTab = insightsTab.cloneNode(true) as HTMLAnchorElement;
81+
82+
$(`#${featureId}`).closest('li').remove();
83+
84+
const insightTabListItem = insightsTab.closest('li');
85+
const perceptorTabListItem =
86+
(insightTabListItem?.cloneNode(true) as HTMLElement | null) ?? document.createElement('li');
87+
const perceptorTab = (perceptorTabListItem.querySelector('a') ?? insightsTab.cloneNode(true)) as HTMLAnchorElement;
88+
if (!perceptorTabListItem.contains(perceptorTab)) {
89+
perceptorTabListItem.appendChild(perceptorTab);
90+
}
91+
2192
delete perceptorTab.dataset.selectedLinks;
93+
delete perceptorTab.dataset.reactNav;
94+
delete perceptorTab.dataset.reactNavAnchor;
95+
delete perceptorTab.dataset.hotkey;
2296
perceptorTab.removeAttribute('aria-current');
2397
perceptorTab.classList.remove('selected');
24-
const perceptorHref = `${insightsTab.href}?redirect=perceptor`;
98+
const perceptorUrl = new URL(insightsTab.href);
99+
perceptorUrl.searchParams.set('redirect', 'perceptor');
100+
const perceptorHref = perceptorUrl.toString();
25101
perceptorTab.href = perceptorHref;
26102
perceptorTab.id = featureId;
27-
perceptorTab.setAttribute('data-tab-item', featureId);
103+
perceptorTab.setAttribute('data-tab-item', 'perceptor');
28104
perceptorTab.setAttribute(
29105
'data-analytics-event',
30106
`{"category":"Underline navbar","action":"Click tab","label":"Perceptor","target":"UNDERLINE_NAV.TAB"}`
@@ -33,51 +109,26 @@ const addPerceptorTab = async (): Promise<void | false> => {
33109
perceptorTitle.text('Perceptor').attr('data-content', 'Perceptor');
34110

35111
// slot for any future counter function
36-
const perceptorCounter = $('[class=Counter]', perceptorTab);
112+
const perceptorCounter = $('[class=Counter], [data-component="counter"]', perceptorTab);
37113
perceptorCounter.attr('id', `${featureId}-count`);
38114

39115
// replace with the perceptor Icon
40116
$('svg.octicon', perceptorTab).html(iconSvgPath);
41117

42118
// add the Perceptor tab to the tabs list
43-
if (!insightsTab.parentElement) {
44-
return false;
45-
}
46-
const tabContainer = document.createElement('li');
47-
tabContainer.appendChild(perceptorTab);
48-
tabContainer.setAttribute('data-view-component', 'true');
49-
tabContainer.className = 'd-inline-flex';
50-
insightsTab.parentElement.after(tabContainer);
51-
52-
// add to drop down menu (when the window is narrow enough some tabs are hidden into "···" menu)
53-
const repoNavigationDropdown = await elementReady('.UnderlineNav-actions ul');
54-
if (!repoNavigationDropdown) {
119+
if (!insightTabListItem?.parentElement) {
55120
return false;
56121
}
57-
const insightsTabDataItem = $('li[data-menu-item$="insights-tab"]', repoNavigationDropdown);
58-
const perceptorTabDataItem = insightsTabDataItem.clone(true);
59-
perceptorTabDataItem.attr('data-menu-item', featureId);
60-
perceptorTabDataItem.children('a').attr({
61-
href: perceptorHref,
62-
});
63-
const perceptorSvgElement = perceptorTabDataItem
64-
.children('a')
65-
.find('span.ActionListItem-visual.ActionListItem-visual--leading')
66-
.find('svg');
67-
perceptorSvgElement.attr('class', 'octicon octicon-perceptor');
68-
perceptorSvgElement.html(iconSvgPath);
69-
const perceptorTextElement = perceptorTabDataItem.children('a').find('span.ActionListItem-label');
70-
perceptorTextElement.text('Perceptor');
71-
insightsTabDataItem.after(perceptorTabDataItem);
122+
insightTabListItem.after(perceptorTabListItem);
72123
// Trigger a reflow to push the right-most tab into the overflow dropdown
73124
window.dispatchEvent(new Event('resize'));
74125
};
75126

76127
const updatePerceptorTabHighlighting = async (): Promise<void> => {
77-
const insightsTab = $('#insights-tab');
128+
const insightsTab = getInsightsTab();
78129
const perceptorTab = $(`#${featureId}`);
79130
// no operation needed
80-
if (!isPerceptor()) return;
131+
if (!isPerceptor() || perceptorTab.length === 0 || insightsTab.length === 0) return;
81132
// if perceptor tab
82133
if (insightsTab.hasClass('selected')) {
83134
insightsTab.removeClass('selected');
@@ -86,6 +137,11 @@ const updatePerceptorTabHighlighting = async (): Promise<void> => {
86137
perceptorTab.addClass('selected');
87138
}
88139

140+
if (insightsTab.attr('aria-current') === 'page') {
141+
insightsTab.removeAttr('aria-current');
142+
perceptorTab.attr('aria-current', 'page');
143+
}
144+
89145
const insightsTabSeletedLinks = insightsTab.attr('data-selected-links');
90146
insightsTab.removeAttr('data-selected-links');
91147
perceptorTab.attr('data-selected-links', 'pulse');
@@ -96,13 +152,62 @@ const updatePerceptorTabHighlighting = async (): Promise<void> => {
96152
perceptorTab.removeAttr('data-selected-links');
97153
};
98154

155+
const syncPerceptorTab = async (): Promise<void> => {
156+
if (syncingPerceptorTab) {
157+
return;
158+
}
159+
160+
syncingPerceptorTab = true;
161+
try {
162+
await addPerceptorTab();
163+
await updatePerceptorTabHighlighting();
164+
} finally {
165+
syncingPerceptorTab = false;
166+
}
167+
};
168+
169+
const observeRepositoryNavigation = async (): Promise<void> => {
170+
const repositoryNavigation = await waitForMeasuredRepositoryNavigation();
171+
if (!repositoryNavigation || repositoryNavigation === observedRepositoryNavigation) {
172+
return;
173+
}
174+
175+
navigationObserver?.disconnect();
176+
observedRepositoryNavigation = repositoryNavigation as HTMLElement;
177+
navigationObserver = new MutationObserver(async () => {
178+
const navigation = getRepositoryNavigation()[0];
179+
if (!navigation || syncingPerceptorTab) {
180+
return;
181+
}
182+
183+
const perceptorTab = document.getElementById(featureId);
184+
if (!perceptorTab) {
185+
await syncPerceptorTab();
186+
return;
187+
}
188+
189+
if (isPerceptor() && perceptorTab.getAttribute('aria-current') !== 'page') {
190+
await updatePerceptorTabHighlighting();
191+
}
192+
});
193+
194+
navigationObserver.observe(repositoryNavigation, {
195+
childList: true,
196+
subtree: true,
197+
});
198+
};
199+
99200
const init = async (): Promise<void> => {
100-
await addPerceptorTab();
201+
await syncPerceptorTab();
202+
await observeRepositoryNavigation();
101203
// TODO need a mechanism to remove extra listeners like this one
102204
// add event listener to update tab highlighting at each turbo:load event
103-
document.addEventListener('turbo:load', async () => {
104-
await updatePerceptorTabHighlighting();
105-
});
205+
if (!highlightingListenerAttached) {
206+
highlightingListenerAttached = true;
207+
document.addEventListener('turbo:load', async () => {
208+
await syncPerceptorTab();
209+
});
210+
}
106211
};
107212

108213
features.add(featureId, {

0 commit comments

Comments
 (0)