1+ import type { StrictlyParseSelector } from 'typed-query-selector/parser.js' ;
12import reservedNames from 'github-reserved-names/reserved-names.json' with { type : 'json' } ;
23import { addTests } from './collector.ts' ;
34
4- const $ = < E extends Element > ( selector : string ) => document . querySelector < E > ( selector ) ;
5- const exists = ( selector : string ) => Boolean ( $ ( selector ) ) ;
5+ function $ < Selector extends string , Selected extends Element = StrictlyParseSelector < Selector > > (
6+ selector : Selector ,
7+ ) : Selected | undefined ;
8+ function $ < Selected extends Element = HTMLElement > (
9+ selector : string ,
10+ ) : Selected | undefined ;
11+ function $ < Selected extends Element > ( selector : string ) : Selected | undefined {
12+ return document . querySelector < Selected > ( selector ) ?? undefined ;
13+ }
14+
15+ function exists < Selector extends string , Selected extends Element = StrictlyParseSelector < Selector > > (
16+ selector : Selector ,
17+ ) : Selected extends never ? never : boolean ;
18+ function exists ( selector : string ) : boolean ;
19+ function exists ( selector : string ) : boolean {
20+ return Boolean ( document . querySelector ( selector ) ) ;
21+ }
22+
23+ /**
24+ * Waits for a detection to return true by repeatedly checking it on each animation frame.
25+ * Useful for DOM-based detections that need to wait for elements to appear.
26+ * @param detection - A detection function to check repeatedly
27+ * @returns A promise that resolves to the final result of the detection
28+ * @example
29+ * ```
30+ * import {utils} from 'github-url-detection';
31+ *
32+ * async function init() {
33+ * if (!await utils.waitFor(isOrganizationProfile)) {
34+ * return;
35+ * }
36+ * // Do something when on organization profile
37+ * }
38+ * ```
39+ */
40+ async function waitFor ( detection : ( ) => boolean ) : Promise < boolean > {
41+ // eslint-disable-next-line no-await-in-loop -- We need to wait on each frame
42+ while ( ! detection ( ) && document . readyState !== 'complete' ) {
43+ // eslint-disable-next-line no-await-in-loop
44+ await new Promise ( resolve => {
45+ requestAnimationFrame ( resolve ) ;
46+ } ) ;
47+ }
48+
49+ return detection ( ) ;
50+ }
651
752const combinedTestOnly = [ 'combinedTestOnly' ] ; // To be used only to skip tests of combined functions, i.e. isPageA() || isPageB()
853
@@ -61,17 +106,19 @@ TEST: addTests('isCompareWikiPage', [
61106 'https://github.com/brookhong/Surfingkeys/wiki/Color-Themes/_compare/8ebb46b1a12d16fc1af442b7df0ca13ca3bb34dc...80e51eeabe69b15a3f23880ecc36f800b71e6c6d' ,
62107] ) ;
63108
109+ /**
110+ * @deprecated Use `isHome` and/or `isFeed` instead
111+ */
64112export const isDashboard = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => ! isGist ( url ) && / ^ $ | ^ ( o r g s \/ [ ^ / ] + \/ ) ? d a s h b o a r d ( - f e e d ) ? ( \/ | $ ) / . test ( processPathname ( url ) ) ;
65113TEST: addTests ( 'isDashboard' , [
66114 'https://github.com///' ,
67115 'https://github.com//' ,
68116 'https://github.com/' ,
69117 'https://github.com' ,
70- 'https://github.com/orgs/test /dashboard' ,
118+ 'https://github.com/orgs/refined-github /dashboard' ,
71119 'https://github.com/dashboard/index/2' ,
72120 'https://github.com//dashboard' ,
73121 'https://github.com/dashboard' ,
74- 'https://github.com/orgs/edit/dashboard' ,
75122 'https://github.big-corp.com/' ,
76123 'https://not-github.com/' ,
77124 'https://my-little-hub.com/' ,
@@ -84,6 +131,31 @@ TEST: addTests('isDashboard', [
84131 'https://github.com/dashboard-feed' ,
85132] ) ;
86133
134+ export const isHome = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => ! isGist ( url ) && / ^ $ | ^ d a s h b o a r d \/ ? $ / . test ( getCleanPathname ( url ) ) ;
135+ TEST: addTests ( 'isHome' , [
136+ 'https://github.com' ,
137+ 'https://github.com//dashboard' ,
138+ 'https://github.com///' ,
139+ 'https://github.com//' ,
140+ 'https://github.com/' ,
141+ 'https://github.com/dashboard' ,
142+ 'https://github.big-corp.com/' ,
143+ 'https://not-github.com/' ,
144+ 'https://my-little-hub.com/' ,
145+ 'https://github.com/?tab=repositories' , // Gotcha for `isUserProfileRepoTab`
146+ 'https://github.com/?tab=stars' , // Gotcha for `isUserProfileStarsTab`
147+ 'https://github.com/?tab=followers' , // Gotcha for `isUserProfileFollowersTab`
148+ 'https://github.com/?tab=following' , // Gotcha for `isUserProfileFollowingTab`
149+ 'https://github.com/?tab=overview' , // Gotcha for `isUserProfileMainTab`
150+ 'https://github.com?search=1' , // Gotcha for `isRepoTree`
151+ ] ) ;
152+
153+ export const isFeed = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => ! isGist ( url ) && / ^ ( f e e d | o r g s \/ [ ^ / ] + \/ d a s h b o a r d ) \/ ? $ / . test ( getCleanPathname ( url ) ) ;
154+ TEST: addTests ( 'isFeed' , [
155+ 'https://github.com/feed' ,
156+ 'https://github.com/orgs/refined-github/dashboard' ,
157+ ] ) ;
158+
87159export const isEnterprise = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => url . hostname !== 'github.com' && url . hostname !== 'gist.github.com' ;
88160TEST: addTests ( 'isEnterprise' , [
89161 'https://github.big-corp.com/' ,
@@ -865,6 +937,9 @@ TEST: addTests('isRepositoryActions', [
865937
866938export const isUserTheOrganizationOwner = ( ) : boolean => isOrganizationProfile ( ) && exists ( '[aria-label="Organization"] [data-tab-item="org-header-settings-tab"]' ) ;
867939
940+ /**
941+ * @deprecated Use canUserAccessRepoSettings or API instead.
942+ */
868943export const canUserAdminRepo = ( ) : boolean => {
869944 const repo = getRepo ( ) ;
870945 return Boolean ( repo && exists ( `:is(${ [
@@ -874,6 +949,9 @@ export const canUserAdminRepo = (): boolean => {
874949 ] . join ( ',' ) } ) a[href="/${ repo . nameWithOwner } /settings"]`) ) ;
875950} ;
876951
952+ // eslint-disable-next-line @typescript-eslint/no-deprecated
953+ export const canUserAccessRepoSettings = canUserAdminRepo ;
954+
877955export const isNewRepo = ( url : URL | HTMLAnchorElement | Location = location ) : boolean => ! isGist ( url ) && ( url . pathname === '/new' || / ^ o r g a n i z a t i o n s \/ [ ^ / ] + \/ r e p o s i t o r i e s \/ n e w $ / . test ( processPathname ( url ) ) ) ;
878956TEST: addTests ( 'isNewRepo' , [
879957 'https://github.com/new' ,
@@ -988,4 +1066,5 @@ export const utils = {
9881066 getCleanGistPathname,
9891067 getRepositoryInfo : getRepo ,
9901068 parseRepoExplorerTitle,
1069+ waitFor,
9911070} ;
0 commit comments