Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/helpers/get-github-repo-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ import * as pageDetect from 'github-url-detection';
import elementReady from 'element-ready';
import { getPlatform } from './get-platform';

const repoSidebarSectionSelectors = [
'.Layout-sidebar .BorderGrid-cell > .hide-sm.hide-md',
'.Layout-sidebar .BorderGrid-cell .hide-sm.hide-md',
'.BorderGrid-cell > .hide-sm.hide-md',
'.BorderGrid-cell .hide-sm.hide-md',
];

const repoSidebarMarkerSelector = [
'a[href$="/stargazers"]',
'a[href$="/watchers"]',
'a[href$="/forks"]',
'a[href$="/activity"]',
'a[href*="/custom-properties"]',
'a[href="#readme-ov-file"]',
'.topic-tag.topic-tag-link',
].join(', ');

const pickFirstVisible = <T extends HTMLElement>(elements: JQuery<T>) => {
const $visibleElements = elements.filter(':visible');
return ($visibleElements.length > 0 ? $visibleElements : elements).first();
};

export function getRepoName() {
const repoNameByUrl = getRepoNameByUrl();
const repoNameByPage = getRepoNameByPage();
Expand Down Expand Up @@ -36,6 +58,27 @@ export function hasRepoContainerHeader() {
return headerElement && !headerElement.attr('hidden');
}

export function getRepoSidebarSection() {
const $sections = $(repoSidebarSectionSelectors.join(','))
.filter((_, element) => !element.closest('details-dialog, template'))
.filter((_, element) => $(element).find(repoSidebarMarkerSelector).length > 0);

return pickFirstVisible($sections);
}

export function getRepoSidebarBorderGrid() {
const $sidebarSection = getRepoSidebarSection();
if ($sidebarSection.length > 0) {
return $sidebarSection.closest('.BorderGrid').first();
}

const $borderGrids = $('.Layout-sidebar .BorderGrid, .BorderGrid')
.filter((_, element) => !element.closest('details-dialog, template'))
.filter((_, element) => $(element).find(repoSidebarMarkerSelector).length > 0);

return pickFirstVisible($borderGrids);
}

export async function isRepoRoot() {
return pageDetect.isRepoRoot();
}
Expand Down
6 changes: 3 additions & 3 deletions src/pages/ContentScripts/features/developer-networks/view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const View = ({ userName }: Props): JSX.Element => {
>
<span
title={`${t('global_clickToshow')} ${t('component_developmentActivityNetwork_title')}`}
className="Label"
className="hypercrx-label"
style={{
color: 'var(--color-fg-default)',
fontWeight: 'var(--base-text-weight-normal, 400)',
Expand Down Expand Up @@ -107,7 +107,7 @@ const View = ({ userName }: Props): JSX.Element => {
>
<span
title={`${t('global_clickToshow')} ${t('component_openSourcePartnersNetwork_title')}`}
className="Label"
className="hypercrx-label"
style={{
color: 'var(--color-fg-default)',
fontWeight: 'var(--base-text-weight-normal, 400)',
Expand Down Expand Up @@ -140,7 +140,7 @@ const View = ({ userName }: Props): JSX.Element => {
>
<span
title={`${t('global_clickToshow')} ${t('component_openSourceInterestsNetwork_title')}`}
className="Label"
className="hypercrx-label"
style={{
color: 'var(--color-fg-default)',
fontWeight: 'var(--base-text-weight-normal, 400)',
Expand Down
183 changes: 144 additions & 39 deletions src/pages/ContentScripts/features/perceptor-tab/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,98 @@ import sleep from '../../../../helpers/sleep';
import isGithub from '../../../../helpers/is-github';

const featureId = features.getFeatureID(import.meta.url);
const insightsTabSelectors = [
'nav[aria-label="Repository"] a[data-tab-item="insights"]',
'a.UnderlineNav-item#insights-tab',
].join(', ');
let highlightingListenerAttached = false;
let syncingPerceptorTab = false;
let navigationObserver: MutationObserver | null = null;
let observedRepositoryNavigation: HTMLElement | null = null;

const getInsightsTab = () => {
const $tabs = $(insightsTabSelectors).filter((_, element) => !element.closest('template'));
const $visibleTabs = $tabs.filter(':visible');

return ($visibleTabs.length > 0 ? $visibleTabs : $tabs).first();
};

const getRepositoryNavigation = () => {
const $navigations = $('nav[aria-label="Repository"]').filter((_, element) => !element.closest('template'));
const $visibleNavigations = $navigations.filter(':visible');

return ($visibleNavigations.length > 0 ? $visibleNavigations : $navigations).first();
};

const waitForMeasuredRepositoryNavigation = async (): Promise<HTMLElement | null> => {
const repositoryNavigation = (await elementReady('nav[aria-label="Repository"]', {
waitForChildren: false,
})) as HTMLElement | null;

if (!repositoryNavigation) {
return null;
}

if (repositoryNavigation.dataset.overflowMeasured === 'true') {
return repositoryNavigation;
}

await new Promise<void>((resolve) => {
let observer: MutationObserver;
const timeout = window.setTimeout(() => {
observer.disconnect();
resolve();
}, 1500);

observer = new MutationObserver(() => {
if (repositoryNavigation.dataset.overflowMeasured === 'true') {
window.clearTimeout(timeout);
observer.disconnect();
resolve();
}
});

observer.observe(repositoryNavigation, {
attributes: true,
attributeFilter: ['data-overflow-measured'],
});
});

return repositoryNavigation;
};

const addPerceptorTab = async (): Promise<void | false> => {
// the creation of the Perceptor tab is based on the Insights tab
const insightsTab = await elementReady('a.UnderlineNav-item[id="insights-tab"]', { waitForChildren: false });
await waitForMeasuredRepositoryNavigation();
await elementReady(insightsTabSelectors, { waitForChildren: false });
const $insightsTab = getInsightsTab();
const insightsTab = $insightsTab[0] as HTMLAnchorElement | undefined;
if (!insightsTab) {
// if the selector failed to find the Insights tab
return false;
}
const perceptorTab = insightsTab.cloneNode(true) as HTMLAnchorElement;

$(`#${featureId}`).closest('li').remove();

const insightTabListItem = insightsTab.closest('li');
const perceptorTabListItem =
(insightTabListItem?.cloneNode(true) as HTMLElement | null) ?? document.createElement('li');
const perceptorTab = (perceptorTabListItem.querySelector('a') ?? insightsTab.cloneNode(true)) as HTMLAnchorElement;
if (!perceptorTabListItem.contains(perceptorTab)) {
perceptorTabListItem.appendChild(perceptorTab);
}

delete perceptorTab.dataset.selectedLinks;
delete perceptorTab.dataset.reactNav;
delete perceptorTab.dataset.reactNavAnchor;
delete perceptorTab.dataset.hotkey;
perceptorTab.removeAttribute('aria-current');
perceptorTab.classList.remove('selected');
const perceptorHref = `${insightsTab.href}?redirect=perceptor`;
const perceptorUrl = new URL(insightsTab.href);
perceptorUrl.searchParams.set('redirect', 'perceptor');
const perceptorHref = perceptorUrl.toString();
perceptorTab.href = perceptorHref;
perceptorTab.id = featureId;
perceptorTab.setAttribute('data-tab-item', featureId);
perceptorTab.setAttribute('data-tab-item', 'perceptor');
perceptorTab.setAttribute(
'data-analytics-event',
`{"category":"Underline navbar","action":"Click tab","label":"Perceptor","target":"UNDERLINE_NAV.TAB"}`
Expand All @@ -33,51 +109,26 @@ const addPerceptorTab = async (): Promise<void | false> => {
perceptorTitle.text('Perceptor').attr('data-content', 'Perceptor');

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

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

// add the Perceptor tab to the tabs list
if (!insightsTab.parentElement) {
return false;
}
const tabContainer = document.createElement('li');
tabContainer.appendChild(perceptorTab);
tabContainer.setAttribute('data-view-component', 'true');
tabContainer.className = 'd-inline-flex';
insightsTab.parentElement.after(tabContainer);

// add to drop down menu (when the window is narrow enough some tabs are hidden into "···" menu)
const repoNavigationDropdown = await elementReady('.UnderlineNav-actions ul');
if (!repoNavigationDropdown) {
if (!insightTabListItem?.parentElement) {
return false;
}
const insightsTabDataItem = $('li[data-menu-item$="insights-tab"]', repoNavigationDropdown);
const perceptorTabDataItem = insightsTabDataItem.clone(true);
perceptorTabDataItem.attr('data-menu-item', featureId);
perceptorTabDataItem.children('a').attr({
href: perceptorHref,
});
const perceptorSvgElement = perceptorTabDataItem
.children('a')
.find('span.ActionListItem-visual.ActionListItem-visual--leading')
.find('svg');
perceptorSvgElement.attr('class', 'octicon octicon-perceptor');
perceptorSvgElement.html(iconSvgPath);
const perceptorTextElement = perceptorTabDataItem.children('a').find('span.ActionListItem-label');
perceptorTextElement.text('Perceptor');
insightsTabDataItem.after(perceptorTabDataItem);
insightTabListItem.after(perceptorTabListItem);
// Trigger a reflow to push the right-most tab into the overflow dropdown
window.dispatchEvent(new Event('resize'));
};

const updatePerceptorTabHighlighting = async (): Promise<void> => {
const insightsTab = $('#insights-tab');
const insightsTab = getInsightsTab();
const perceptorTab = $(`#${featureId}`);
// no operation needed
if (!isPerceptor()) return;
if (!isPerceptor() || perceptorTab.length === 0 || insightsTab.length === 0) return;
// if perceptor tab
if (insightsTab.hasClass('selected')) {
insightsTab.removeClass('selected');
Expand All @@ -86,6 +137,11 @@ const updatePerceptorTabHighlighting = async (): Promise<void> => {
perceptorTab.addClass('selected');
}

if (insightsTab.attr('aria-current') === 'page') {
insightsTab.removeAttr('aria-current');
perceptorTab.attr('aria-current', 'page');
}

const insightsTabSeletedLinks = insightsTab.attr('data-selected-links');
insightsTab.removeAttr('data-selected-links');
perceptorTab.attr('data-selected-links', 'pulse');
Expand All @@ -96,13 +152,62 @@ const updatePerceptorTabHighlighting = async (): Promise<void> => {
perceptorTab.removeAttr('data-selected-links');
};

const syncPerceptorTab = async (): Promise<void> => {
if (syncingPerceptorTab) {
return;
}

syncingPerceptorTab = true;
try {
await addPerceptorTab();
await updatePerceptorTabHighlighting();
} finally {
syncingPerceptorTab = false;
}
};

const observeRepositoryNavigation = async (): Promise<void> => {
const repositoryNavigation = await waitForMeasuredRepositoryNavigation();
if (!repositoryNavigation || repositoryNavigation === observedRepositoryNavigation) {
return;
}

navigationObserver?.disconnect();
observedRepositoryNavigation = repositoryNavigation as HTMLElement;
navigationObserver = new MutationObserver(async () => {
const navigation = getRepositoryNavigation()[0];
if (!navigation || syncingPerceptorTab) {
return;
}

const perceptorTab = document.getElementById(featureId);
if (!perceptorTab) {
await syncPerceptorTab();
return;
}

if (isPerceptor() && perceptorTab.getAttribute('aria-current') !== 'page') {
await updatePerceptorTabHighlighting();
}
});

navigationObserver.observe(repositoryNavigation, {
childList: true,
subtree: true,
});
};

const init = async (): Promise<void> => {
await addPerceptorTab();
await syncPerceptorTab();
await observeRepositoryNavigation();
// TODO need a mechanism to remove extra listeners like this one
// add event listener to update tab highlighting at each turbo:load event
document.addEventListener('turbo:load', async () => {
await updatePerceptorTabHighlighting();
});
if (!highlightingListenerAttached) {
highlightingListenerAttached = true;
document.addEventListener('turbo:load', async () => {
await syncPerceptorTab();
});
}
};

features.add(featureId, {
Expand Down
Loading
Loading