2025-10-28 18:25:00 +08:00
import { GET , POST } from '../modules/fetch.ts' ;
2026-03-29 12:24:30 +02:00
import { showGlobalErrorMessage } from '../modules/errors.ts' ;
2024-11-08 14:04:24 +08:00
import { fomanticQuery } from '../modules/fomantic/base.ts' ;
2025-10-28 18:25:00 +08:00
import { addDelegatedEventListener , queryElems } from '../utils/dom.ts' ;
2025-03-03 10:57:28 +08:00
import { registerGlobalInitFunc , registerGlobalSelectorFunc } from '../modules/observer.ts' ;
2025-04-19 08:17:07 +08:00
import { initAvatarUploaderWithCropper } from './comp/Cropper.ts' ;
2025-09-10 16:50:17 -07:00
import { initCompSearchRepoBox } from './comp/SearchRepoBox.ts' ;
2024-06-21 15:40:33 +08:00
2025-10-28 18:25:00 +08:00
const { appUrl , appSubUrl } = window . config ;
2024-06-21 15:40:33 +08:00
2025-10-28 18:25:00 +08:00
function initHeadNavbarContentToggle() {
2024-06-21 15:40:33 +08:00
const navbar = document . querySelector ( '#navbar' ) ;
const btn = document . querySelector ( '#navbar-expand-toggle' ) ;
if ( ! navbar || ! btn ) return ;
btn . addEventListener ( 'click' , ( ) = > {
const isExpanded = btn . classList . contains ( 'active' ) ;
navbar . classList . toggle ( 'navbar-menu-open' , ! isExpanded ) ;
btn . classList . toggle ( 'active' , ! isExpanded ) ;
} ) ;
}
2025-10-28 18:25:00 +08:00
function initFooterLanguageMenu() {
2024-11-08 14:04:24 +08:00
document . querySelector ( '.ui.dropdown .menu.language-menu' ) ? . addEventListener ( 'click' , async ( e ) = > {
const item = ( e . target as HTMLElement ) . closest ( '.item' ) ;
if ( ! item ) return ;
e . preventDefault ( ) ;
2025-12-03 03:13:16 +01:00
await GET ( item . getAttribute ( 'data-url' ) ! ) ;
2024-06-21 15:40:33 +08:00
window . location . reload ( ) ;
2024-11-08 14:04:24 +08:00
} ) ;
2024-06-21 15:40:33 +08:00
}
2025-10-28 18:25:00 +08:00
function initFooterThemeSelector() {
const elDropdown = document . querySelector ( '#footer-theme-selector' ) ;
if ( ! elDropdown ) return ; // some pages don't have footer, for example: 500.tmpl
const $dropdown = fomanticQuery ( elDropdown ) ;
$dropdown . dropdown ( {
direction : 'upward' ,
apiSettings : { url : ` ${ appSubUrl } /-/web-theme/list ` , cache : false } ,
} ) ;
addDelegatedEventListener ( elDropdown , 'click' , '.menu > .item' , async ( el ) = > {
2025-12-03 03:13:16 +01:00
const themeName = el . getAttribute ( 'data-value' ) ! ;
2025-10-28 18:25:00 +08:00
await POST ( ` ${ appSubUrl } /-/web-theme/apply?theme= ${ encodeURIComponent ( themeName ) } ` ) ;
window . location . reload ( ) ;
} ) ;
}
export function initCommmPageComponents() {
initHeadNavbarContentToggle ( ) ;
initFooterLanguageMenu ( ) ;
initFooterThemeSelector ( ) ;
}
2024-06-22 12:52:09 +08:00
export function initGlobalDropdown() {
2024-06-21 15:40:33 +08:00
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
2025-03-03 10:57:28 +08:00
registerGlobalSelectorFunc ( '.ui.dropdown:not(.custom)' , ( el ) = > {
2025-03-01 10:02:10 +08:00
const $dropdown = fomanticQuery ( el ) ;
if ( $dropdown . data ( 'module-dropdown' ) ) return ; // do not re-init if other code has already initialized it.
2024-06-21 15:40:33 +08:00
2025-03-01 10:02:10 +08:00
$dropdown . dropdown ( 'setting' , { hideDividers : 'empty' } ) ;
2024-06-21 15:40:33 +08:00
2025-03-01 10:02:10 +08:00
if ( el . classList . contains ( 'jump' ) ) {
// The "jump" means this dropdown is mainly used for "menu" purpose,
// clicking an item will jump to somewhere else or trigger an action/function.
// When a dropdown is used for non-refresh actions with tippy,
// it must have this "jump" class to hide the tippy when dropdown is closed.
$dropdown . dropdown ( 'setting' , {
action : 'hide' ,
onShow() {
// hide associated tooltip while dropdown is open
this . _tippy ? . hide ( ) ;
this . _tippy ? . disable ( ) ;
} ,
onHide() {
this . _tippy ? . enable ( ) ;
// eslint-disable-next-line unicorn/no-this-assignment
const elDropdown = this ;
2024-06-21 15:40:33 +08:00
2025-03-01 10:02:10 +08:00
// hide all tippy elements of items after a while. eg: use Enter to click "Copy Link" in the Issue Context Menu
setTimeout ( ( ) = > {
const $dropdown = fomanticQuery ( elDropdown ) ;
if ( $dropdown . dropdown ( 'is hidden' ) ) {
queryElems ( elDropdown , '.menu > .item' , ( el ) = > el . _tippy ? . hide ( ) ) ;
}
} , 2000 ) ;
} ,
} ) ;
}
// Special popup-directions, prevent Fomantic from guessing the popup direction.
// With default "direction: auto", if the viewport height is small, Fomantic would show the popup upward,
// if the dropdown is at the beginning of the page, then the top part would be clipped by the window view.
// eg: Issue List "Sort" dropdown
// But we can not set "direction: downward" for all dropdowns, because there is a bug in dropdown menu positioning when calculating the "left" position,
// which would make some dropdown popups slightly shift out of the right viewport edge in some cases.
// eg: the "Create New Repo" menu on the navbar.
if ( el . classList . contains ( 'upward' ) ) $dropdown . dropdown ( 'setting' , 'direction' , 'upward' ) ;
if ( el . classList . contains ( 'downward' ) ) $dropdown . dropdown ( 'setting' , 'direction' , 'downward' ) ;
} ) ;
2024-06-22 12:52:09 +08:00
}
2024-06-21 15:40:33 +08:00
2025-09-10 16:50:17 -07:00
export function initGlobalComponent() {
2025-03-11 12:44:52 +08:00
fomanticQuery ( '.ui.menu.tabular:not(.custom) .item' ) . tab ( ) ;
2025-04-19 08:17:07 +08:00
registerGlobalInitFunc ( 'initAvatarUploader' , initAvatarUploaderWithCropper ) ;
2025-09-10 16:50:17 -07:00
registerGlobalInitFunc ( 'initSearchRepoBox' , initCompSearchRepoBox ) ;
2025-04-19 08:17:07 +08:00
}
2025-03-03 10:57:28 +08:00
// for performance considerations, it only uses performant syntax
function attachInputDirAuto ( el : Partial < HTMLInputElement | HTMLTextAreaElement > ) {
if ( el . type !== 'hidden' &&
el . type !== 'checkbox' &&
el . type !== 'radio' &&
el . type !== 'range' &&
el . type !== 'color' ) {
el . dir = 'auto' ;
}
}
2026-02-17 09:06:27 +01:00
function autoFocusEnd ( el : HTMLInputElement | HTMLTextAreaElement ) {
el . focus ( ) ;
el . setSelectionRange ( el . value . length , el . value . length ) ;
}
export function applyAutoFocus ( container : Element ) {
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autofocus
// "autofocus" behavior is defined by the standard: when a container (e.g.: dialog) becomes visible, focus the element with "autofocus" attribute
// Fomantic UI already supports it for its modal dialog, we need to cover more cases (e.g.: ".show-panel" button)
// Here is just a simple support, we don't expect more than one element that need "autofocus" appearing in the same container
container . querySelector < HTMLElement > ( '[autofocus]' ) ? . focus ( ) ;
// Also, apply our autoFocusEnd behavior
// TODO: GLOBAL-INIT-MULTIPLE-FUNCTIONS: use "~=" operator in case we would extend the "data-global-init" to support more functions in the future.
const el = container . querySelector < HTMLInputElement > ( '[data-global-init~="autoFocusEnd"]' ) ;
if ( el ) autoFocusEnd ( el ) ;
}
2025-03-03 10:57:28 +08:00
export function initGlobalInput() {
registerGlobalSelectorFunc ( 'input, textarea' , attachInputDirAuto ) ;
2026-02-17 09:06:27 +01:00
// autoFocusEnd is used for autofocus an input/textarea and move the cursor to the end of the text.
// It is useful for "New Issue"/"New PR" pages when the title is pre-filled with prefix text (e.g.: from template or commit message)
// The native "autofocus" isn't used because there is a delay between "focused (DOM rendering)" and "move cursor to end (our JS)", it causes flickers.
registerGlobalInitFunc ( 'autoFocusEnd' , autoFocusEnd ) ;
2025-03-03 10:57:28 +08:00
}
2024-06-21 15:40:33 +08:00
/**
* Too many users set their ROOT_URL to wrong value, and it causes a lot of problems:
* * Cross-origin API request without correct cookie
* * Incorrect href in <a>
* * ...
* So we check whether current URL starts with AppUrl(ROOT_URL).
* If they don't match, show a warning to users.
*/
export function checkAppUrl() {
const curUrl = window . location . href ;
// some users visit "https://domain/gitea" while appUrl is "https://domain/gitea/", there should be no warning
if ( curUrl . startsWith ( appUrl ) || ` ${ curUrl } / ` === appUrl ) {
return ;
}
showGlobalErrorMessage ( ` Your ROOT_URL in app.ini is " ${ appUrl } ", it's unlikely matching the site you are visiting.
Mismatched ROOT_URL config causes wrong URL links for web UI/mail content/webhook notification/OAuth2 sign-in. ` , 'warning' ) ;
}
2024-10-17 10:28:51 +08:00
export function checkAppUrlScheme() {
const curUrl = window . location . href ;
// some users visit "http://domain" while appUrl is "https://domain", COOKIE_SECURE makes it impossible to sign in
if ( curUrl . startsWith ( 'http:' ) && appUrl . startsWith ( 'https:' ) ) {
showGlobalErrorMessage ( ` This instance is configured to run under HTTPS (by ROOT_URL config), you are accessing by HTTP. Mismatched scheme might cause problems for sign-in/sign-up. ` , 'warning' ) ;
}
}