2024-07-07 17:32:30 +02:00
import { svg } from '../svg.ts' ;
import { createTippy } from '../modules/tippy.ts' ;
import { toAbsoluteUrl } from '../utils.ts' ;
2025-01-04 10:56:07 +08:00
import { addDelegatedEventListener } from '../utils/dom.ts' ;
2022-11-04 21:33:50 +02:00
2024-12-11 09:29:04 +01:00
function changeHash ( hash : string ) {
2021-10-17 01:28:04 +08:00
if ( window . history . pushState ) {
2025-12-03 03:13:16 +01:00
window . history . pushState ( null , '' , hash ) ;
2021-10-17 01:28:04 +08:00
} else {
window . location . hash = hash ;
}
}
2025-01-04 10:56:07 +08:00
// it selects the code lines defined by range: `L1-L3` (3 lines) or `L2` (singe line)
2025-12-03 03:13:16 +01:00
function selectRange ( range : string ) : Element | null {
2025-01-04 10:56:07 +08:00
for ( const el of document . querySelectorAll ( '.code-view tr.active' ) ) el . classList . remove ( 'active' ) ;
const elLineNums = document . querySelectorAll ( ` .code-view td.lines-num span[data-line-number] ` ) ;
2024-03-24 13:14:03 +01:00
2024-03-26 01:03:12 +02:00
const refInNewIssue = document . querySelector ( 'a.ref-in-new-issue' ) ;
const copyPermalink = document . querySelector ( 'a.copy-line-permalink' ) ;
const viewGitBlame = document . querySelector ( 'a.view_git_blame' ) ;
2021-10-17 01:28:04 +08:00
2024-12-11 09:29:04 +01:00
const updateIssueHref = function ( anchor : string ) {
2024-03-26 01:03:12 +02:00
if ( ! refInNewIssue ) return ;
const urlIssueNew = refInNewIssue . getAttribute ( 'data-url-issue-new' ) ;
2025-12-03 03:13:16 +01:00
const urlParamBodyLink = refInNewIssue . getAttribute ( 'data-url-param-body-link' ) ! ;
2023-02-08 00:08:44 +08:00
const issueContent = ` ${ toAbsoluteUrl ( urlParamBodyLink ) } # ${ anchor } ` ; // the default content for issue body
2024-03-26 01:03:12 +02:00
refInNewIssue . setAttribute ( 'href' , ` ${ urlIssueNew } ?body= ${ encodeURIComponent ( issueContent ) } ` ) ;
2021-10-17 01:28:04 +08:00
} ;
2024-12-11 09:29:04 +01:00
const updateViewGitBlameFragment = function ( anchor : string ) {
2024-03-26 01:03:12 +02:00
if ( ! viewGitBlame ) return ;
2025-12-03 03:13:16 +01:00
let href = viewGitBlame . getAttribute ( 'href' ) ! ;
2022-04-26 18:54:40 +08:00
href = ` ${ href . replace ( /#L\d+$|#L\d+-L\d+$/ , '' ) } ` ;
if ( anchor . length !== 0 ) {
href = ` ${ href } # ${ anchor } ` ;
}
2024-03-26 01:03:12 +02:00
viewGitBlame . setAttribute ( 'href' , href ) ;
2022-04-26 18:54:40 +08:00
} ;
2024-12-11 09:29:04 +01:00
const updateCopyPermalinkUrl = function ( anchor : string ) {
2024-03-26 01:03:12 +02:00
if ( ! copyPermalink ) return ;
2025-12-03 03:13:16 +01:00
let link = copyPermalink . getAttribute ( 'data-url' ) ! ;
2021-10-17 01:28:04 +08:00
link = ` ${ link . replace ( /#L\d+$|#L\d+-L\d+$/ , '' ) } # ${ anchor } ` ;
2025-04-19 16:43:22 +08:00
copyPermalink . setAttribute ( 'data-clipboard-text' , link ) ;
copyPermalink . setAttribute ( 'data-clipboard-text-type' , 'url' ) ;
2021-10-17 01:28:04 +08:00
} ;
2025-01-04 10:56:07 +08:00
const rangeFields = range ? range . split ( '-' ) : [ ] ;
const start = rangeFields [ 0 ] ? ? '' ;
if ( ! start ) return null ;
const stop = rangeFields [ 1 ] || start ;
// format is i.e. 'L14-L26'
let startLineNum = parseInt ( start . substring ( 1 ) ) ;
let stopLineNum = parseInt ( stop . substring ( 1 ) ) ;
if ( startLineNum > stopLineNum ) {
const tmp = startLineNum ;
startLineNum = stopLineNum ;
stopLineNum = tmp ;
range = ` ${ stop } - ${ start } ` ;
2021-10-17 01:28:04 +08:00
}
2025-01-04 10:56:07 +08:00
const first = elLineNums [ startLineNum - 1 ] ? ? null ;
for ( let i = startLineNum - 1 ; i <= stopLineNum - 1 && i < elLineNums . length ; i ++ ) {
2025-12-03 03:13:16 +01:00
elLineNums [ i ] . closest ( 'tr' ) ! . classList . add ( 'active' ) ;
2025-01-04 10:56:07 +08:00
}
changeHash ( ` # ${ range } ` ) ;
updateIssueHref ( range ) ;
updateViewGitBlameFragment ( range ) ;
updateCopyPermalinkUrl ( range ) ;
return first ;
2021-10-17 01:28:04 +08:00
}
function showLineButton() {
2022-08-09 14:37:34 +02:00
const menu = document . querySelector ( '.code-line-menu' ) ;
if ( ! menu ) return ;
// remove all other line buttons
for ( const el of document . querySelectorAll ( '.code-line-button' ) ) {
el . remove ( ) ;
}
// find active row and add button
2024-03-24 13:14:03 +01:00
const tr = document . querySelector ( '.code-view tr.active' ) ;
2025-01-04 10:56:07 +08:00
if ( ! tr ) return ;
2025-12-03 03:13:16 +01:00
const td = tr . querySelector ( 'td.lines-num' ) ! ;
2022-08-09 14:37:34 +02:00
const btn = document . createElement ( 'button' ) ;
2024-03-24 13:14:03 +01:00
btn . classList . add ( 'code-line-button' , 'ui' , 'basic' , 'button' ) ;
2022-08-09 14:37:34 +02:00
btn . innerHTML = svg ( 'octicon-kebab-horizontal' ) ;
td . prepend ( btn ) ;
// put a copy of the menu back into DOM for the next click
2025-12-03 03:13:16 +01:00
btn . closest ( '.code-view' ) ! . append ( menu . cloneNode ( true ) ) ;
2022-08-09 14:37:34 +02:00
createTippy ( btn , {
2024-04-30 16:52:46 +02:00
theme : 'menu' ,
2022-08-09 14:37:34 +02:00
trigger : 'click' ,
2023-06-09 11:10:51 +02:00
hideOnClick : true ,
2022-08-09 14:37:34 +02:00
content : menu ,
placement : 'right-start' ,
2023-06-14 16:01:37 +08:00
interactive : true ,
2023-06-09 11:10:51 +02:00
onShow : ( tippy ) = > {
tippy . popper . addEventListener ( 'click' , ( ) = > {
tippy . hide ( ) ;
} , { once : true } ) ;
2024-03-22 15:06:53 +01:00
} ,
2022-08-09 14:37:34 +02:00
} ) ;
2021-10-17 01:28:04 +08:00
}
export function initRepoCodeView() {
2025-06-02 09:52:12 +08:00
// When viewing a file or blame, there is always a ".file-view" element,
// but the ".code-view" class is only present when viewing the "code" of a file; it is not present when viewing a PDF file.
// Since the ".file-view" will be dynamically reloaded when navigating via the left file tree (eg: view a PDF file, then view a source code file, etc.)
// the "code-view" related event listeners should always be added when the current page contains ".file-view" element.
if ( ! document . querySelector ( '.repo-view-container .file-view' ) ) return ;
2025-01-04 10:56:07 +08:00
2025-06-02 09:52:12 +08:00
// "file code view" and "blame" pages need this "line number button" feature
2025-12-03 03:13:16 +01:00
let selRangeStart : string | undefined ;
2025-06-02 09:52:12 +08:00
addDelegatedEventListener ( document , 'click' , '.code-view .lines-num span' , ( el : HTMLElement , e : KeyboardEvent ) = > {
2025-01-04 10:56:07 +08:00
if ( ! selRangeStart || ! e . shiftKey ) {
2025-12-03 03:13:16 +01:00
selRangeStart = el . getAttribute ( 'id' ) ! ;
2025-01-04 10:56:07 +08:00
selectRange ( selRangeStart ) ;
} else {
const selRangeStop = el . getAttribute ( 'id' ) ;
selectRange ( ` ${ selRangeStart } - ${ selRangeStop } ` ) ;
}
2025-12-03 03:13:16 +01:00
window . getSelection ( ) ! . removeAllRanges ( ) ;
2025-01-04 10:56:07 +08:00
showLineButton ( ) ;
2021-10-17 01:28:04 +08:00
} ) ;
2025-01-04 10:56:07 +08:00
2025-06-02 09:52:12 +08:00
// apply the selected range from the URL hash
2025-01-04 10:56:07 +08:00
const onHashChange = ( ) = > {
if ( ! window . location . hash ) return ;
2025-06-02 09:52:12 +08:00
if ( ! document . querySelector ( '.code-view .lines-num' ) ) return ;
2025-01-04 10:56:07 +08:00
const range = window . location . hash . substring ( 1 ) ;
const first = selectRange ( range ) ;
if ( first ) {
2025-06-02 09:52:12 +08:00
// set scrollRestoration to 'manual' when there is a hash in the URL, so that the scroll position will not be remembered after refreshing
2025-01-04 10:56:07 +08:00
if ( window . history . scrollRestoration !== 'manual' ) window . history . scrollRestoration = 'manual' ;
first . scrollIntoView ( { block : 'start' } ) ;
showLineButton ( ) ;
}
} ;
onHashChange ( ) ;
window . addEventListener ( 'hashchange' , onHashChange ) ;
2021-10-17 01:28:04 +08:00
}