2025-03-01 10:02:10 +08:00
import { isDocumentFragmentOrElementNode } from '../utils/dom.ts' ;
2025-09-15 16:34:54 +02:00
import type { Promisable } from '../types.ts' ;
2025-03-03 10:57:28 +08:00
import type { InitPerformanceTracer } from './init.ts' ;
2025-03-01 10:02:10 +08:00
2025-03-03 10:57:28 +08:00
let globalSelectorObserverInited = false ;
2025-03-01 10:02:10 +08:00
2025-03-03 10:57:28 +08:00
type SelectorHandler = { selector : string , handler : ( el : HTMLElement ) = > void } ;
const selectorHandlers : SelectorHandler [ ] = [ ] ;
type GlobalEventFunc < T extends HTMLElement , E extends Event > = ( el : T , e : E ) = > Promisable < void > ;
const globalEventFuncs : Record < string , GlobalEventFunc < HTMLElement , Event > > = { } ;
type GlobalInitFunc < T extends HTMLElement > = ( el : T ) = > Promisable < void > ;
const globalInitFuncs : Record < string , GlobalInitFunc < HTMLElement > > = { } ;
// It handles the global events for all `<div data-global-click="onSomeElemClick"></div>` elements.
export function registerGlobalEventFunc < T extends HTMLElement , E extends Event > ( event : string , name : string , func : GlobalEventFunc < T , E > ) {
globalEventFuncs [ ` ${ event } : ${ name } ` ] = func as GlobalEventFunc < HTMLElement , Event > ;
}
// It handles the global init functions by a selector, for example:
// > registerGlobalSelectorObserver('.ui.dropdown:not(.custom)', (el) => { initDropdown(el, ...) });
2025-03-04 03:49:15 +08:00
// ATTENTION: For most cases, it's recommended to use registerGlobalInitFunc instead,
// Because this selector-based approach is less efficient and less maintainable.
// But if there are already a lot of elements on many pages, this selector-based approach is more convenient for exiting code.
2025-03-03 10:57:28 +08:00
export function registerGlobalSelectorFunc ( selector : string , handler : ( el : HTMLElement ) = > void ) {
selectorHandlers . push ( { selector , handler } ) ;
// Then initAddedElementObserver will call this handler for all existing elements after all handlers are added.
// This approach makes the init stage only need to do one "querySelectorAll".
if ( ! globalSelectorObserverInited ) return ;
for ( const el of document . querySelectorAll < HTMLElement > ( selector ) ) {
handler ( el ) ;
2025-03-01 10:02:10 +08:00
}
}
2025-03-03 10:57:28 +08:00
// It handles the global init functions for all `<div data-global-int="initSomeElem"></div>` elements.
export function registerGlobalInitFunc < T extends HTMLElement > ( name : string , handler : GlobalInitFunc < T > ) {
globalInitFuncs [ name ] = handler as GlobalInitFunc < HTMLElement > ;
// The "global init" functions are managed internally and called by callGlobalInitFunc
// They must be ready before initGlobalSelectorObserver is called.
if ( globalSelectorObserverInited ) throw new Error ( 'registerGlobalInitFunc() must be called before initGlobalSelectorObserver()' ) ;
}
function callGlobalInitFunc ( el : HTMLElement ) {
2026-02-17 09:06:27 +01:00
// TODO: GLOBAL-INIT-MULTIPLE-FUNCTIONS: maybe in the future we need to extend it to support multiple functions, for example: `data-global-init="func1 func2 func3"`
2025-12-03 03:13:16 +01:00
const initFunc = el . getAttribute ( 'data-global-init' ) ! ;
2025-03-01 10:02:10 +08:00
const func = globalInitFuncs [ initFunc ] ;
if ( ! func ) throw new Error ( ` Global init function " ${ initFunc } " not found ` ) ;
2025-03-03 10:57:28 +08:00
2025-03-26 10:51:22 +08:00
// when an element node is removed and added again, it should not be re-initialized again.
2025-03-03 10:57:28 +08:00
type GiteaGlobalInitElement = Partial < HTMLElement > & { _giteaGlobalInited : boolean } ;
2025-03-26 10:51:22 +08:00
if ( ( el as GiteaGlobalInitElement ) . _giteaGlobalInited ) return ;
2025-03-03 10:57:28 +08:00
( el as GiteaGlobalInitElement ) . _giteaGlobalInited = true ;
2025-03-26 10:51:22 +08:00
2025-03-01 10:02:10 +08:00
func ( el ) ;
}
2025-03-03 10:57:28 +08:00
function attachGlobalEvents() {
// add global "[data-global-click]" event handler
document . addEventListener ( 'click' , ( e ) = > {
const elem = ( e . target as HTMLElement ) . closest < HTMLElement > ( '[data-global-click]' ) ;
if ( ! elem ) return ;
const funcName = elem . getAttribute ( 'data-global-click' ) ;
const func = globalEventFuncs [ ` click: ${ funcName } ` ] ;
if ( ! func ) throw new Error ( ` Global event function "click: ${ funcName } " not found ` ) ;
func ( elem , e ) ;
} ) ;
2025-03-01 10:02:10 +08:00
}
2025-12-03 03:13:16 +01:00
export function initGlobalSelectorObserver ( perfTracer : InitPerformanceTracer | null ) : void {
2025-03-03 10:57:28 +08:00
if ( globalSelectorObserverInited ) throw new Error ( 'initGlobalSelectorObserver() already called' ) ;
globalSelectorObserverInited = true ;
2025-03-01 10:02:10 +08:00
2025-03-03 10:57:28 +08:00
attachGlobalEvents ( ) ;
2025-03-01 10:02:10 +08:00
2025-03-03 10:57:28 +08:00
selectorHandlers . push ( { selector : '[data-global-init]' , handler : callGlobalInitFunc } ) ;
2025-03-01 10:02:10 +08:00
const observer = new MutationObserver ( ( mutationList ) = > {
const len = mutationList . length ;
for ( let i = 0 ; i < len ; i ++ ) {
const mutation = mutationList [ i ] ;
const len = mutation . addedNodes . length ;
for ( let i = 0 ; i < len ; i ++ ) {
const addedNode = mutation . addedNodes [ i ] as HTMLElement ;
if ( ! isDocumentFragmentOrElementNode ( addedNode ) ) continue ;
for ( const { selector , handler } of selectorHandlers ) {
if ( addedNode . matches ( selector ) ) {
handler ( addedNode ) ;
}
2025-03-03 10:57:28 +08:00
for ( const el of addedNode . querySelectorAll < HTMLElement > ( selector ) ) {
2025-03-01 10:02:10 +08:00
handler ( el ) ;
}
}
}
}
} ) ;
2025-03-03 10:57:28 +08:00
if ( perfTracer ) {
for ( const { selector , handler } of selectorHandlers ) {
perfTracer . recordCall ( ` initGlobalSelectorObserver ${ selector } ` , ( ) = > {
for ( const el of document . querySelectorAll < HTMLElement > ( selector ) ) {
handler ( el ) ;
}
} ) ;
}
} else {
for ( const { selector , handler } of selectorHandlers ) {
for ( const el of document . querySelectorAll < HTMLElement > ( selector ) ) {
handler ( el ) ;
}
2025-03-01 10:02:10 +08:00
}
}
observer . observe ( document , { subtree : true , childList : true } ) ;
}