diff options
author | wxiaoguang <wxiaoguang@gmail.com> | 2025-03-03 10:57:28 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-03-03 10:57:28 +0800 |
commit | 27bf63ad20e3241d48636064f585a6d432bdcaa7 (patch) | |
tree | ab18faca2e931374b628d9ff4c09aa9d151411ae /web_src/js | |
parent | 5cbdf83f706ecdace65e5b42897ebbde82c3a0a1 (diff) | |
download | gitea-27bf63ad20e3241d48636064f585a6d432bdcaa7.tar.gz gitea-27bf63ad20e3241d48636064f585a6d432bdcaa7.zip |
Refactor global init code and add more comments (#33755)
Follow up #33748
Now there are 3 "global" functions:
* registerGlobalSelectorFunc: for all elements matching the selector, eg: `.ui.dropdown`
* registerGlobalInitFunc: for `data-global-init="initInputAutoFocusEnd"`
* registerGlobalEventFunc: for `data-global-click="onCommentReactionButtonClick"`
And introduce `initGlobalInput` to replace old `initAutoFocusEnd` and
`attachDirAuto`, use `data-global-init` to replace fragile
`.js-autofocus-end` selector.
Another benefit is that by the new approach, no matter how many times
`registerGlobalInitFunc` is called, we only need to do one
"querySelectorAll" in the last step, it could slightly improve the
performance.
Diffstat (limited to 'web_src/js')
-rw-r--r-- | web_src/js/features/autofocus-end.ts | 6 | ||||
-rw-r--r-- | web_src/js/features/common-page.ts | 23 | ||||
-rw-r--r-- | web_src/js/features/repo-diff.ts | 4 | ||||
-rw-r--r-- | web_src/js/index.ts | 65 | ||||
-rw-r--r-- | web_src/js/modules/init.ts | 26 | ||||
-rw-r--r-- | web_src/js/modules/observer.ts | 118 | ||||
-rw-r--r-- | web_src/js/utils/dom.ts | 2 |
7 files changed, 135 insertions, 109 deletions
diff --git a/web_src/js/features/autofocus-end.ts b/web_src/js/features/autofocus-end.ts deleted file mode 100644 index 53e475b543..0000000000 --- a/web_src/js/features/autofocus-end.ts +++ /dev/null @@ -1,6 +0,0 @@ -export function initAutoFocusEnd() { - for (const el of document.querySelectorAll<HTMLInputElement>('.js-autofocus-end')) { - el.focus(); // expects only one such element on one page. If there are many, then the last one gets the focus. - el.setSelectionRange(el.value.length, el.value.length); - } -} diff --git a/web_src/js/features/common-page.ts b/web_src/js/features/common-page.ts index c5274201f2..235555a73d 100644 --- a/web_src/js/features/common-page.ts +++ b/web_src/js/features/common-page.ts @@ -2,7 +2,7 @@ import {GET} from '../modules/fetch.ts'; import {showGlobalErrorMessage} from '../bootstrap.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; import {queryElems} from '../utils/dom.ts'; -import {observeAddedElement} from '../modules/observer.ts'; +import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts'; const {appUrl} = window.config; @@ -30,7 +30,7 @@ export function initFootLanguageMenu() { export function initGlobalDropdown() { // do not init "custom" dropdowns, "custom" dropdowns are managed by their own code. - observeAddedElement('.ui.dropdown:not(.custom)', (el) => { + registerGlobalSelectorFunc('.ui.dropdown:not(.custom)', (el) => { const $dropdown = fomanticQuery(el); if ($dropdown.data('module-dropdown')) return; // do not re-init if other code has already initialized it. @@ -80,6 +80,25 @@ export function initGlobalTabularMenu() { fomanticQuery('.ui.menu.tabular:not(.custom) .item').tab({autoTabActivation: false}); } +// 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'; + } +} + +export function initGlobalInput() { + registerGlobalSelectorFunc('input, textarea', attachInputDirAuto); + registerGlobalInitFunc('initInputAutoFocusEnd', (el: HTMLInputElement) => { + el.focus(); // expects only one such element on one page. If there are many, then the last one gets the focus. + el.setSelectionRange(el.value.length, el.value.length); + }); +} + /** * Too many users set their ROOT_URL to wrong value, and it causes a lot of problems: * * Cross-origin API request without correct cookie diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index f1e441a133..1ecd00f1af 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -10,7 +10,7 @@ import {POST, GET} from '../modules/fetch.ts'; import {createTippy} from '../modules/tippy.ts'; import {invertFileFolding} from './file-fold.ts'; import {parseDom} from '../utils.ts'; -import {observeAddedElement} from '../modules/observer.ts'; +import {registerGlobalSelectorFunc} from '../modules/observer.ts'; const {i18n} = window.config; @@ -254,7 +254,7 @@ export function initRepoDiffView() { initExpandAndCollapseFilesButton(); initRepoDiffHashChangeListener(); - observeAddedElement('#diff-file-boxes .diff-file-box', initRepoDiffFileBox); + registerGlobalSelectorFunc('#diff-file-boxes .diff-file-box', initRepoDiffFileBox); addDelegatedEventListener(document, 'click', '.fold-file', (el) => { invertFileFolding(el.closest('.file-content'), el); }); diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 2e253870c0..f48074316e 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -11,7 +11,6 @@ import {initImageDiff} from './features/imagediff.ts'; import {initRepoMigration} from './features/repo-migration.ts'; import {initRepoProject} from './features/repo-projects.ts'; import {initTableSort} from './features/tablesort.ts'; -import {initAutoFocusEnd} from './features/autofocus-end.ts'; import {initAdminUserListSearchForm} from './features/admin/users.ts'; import {initAdminConfigs} from './features/admin/config.ts'; import {initMarkupAnchors} from './markup/anchors.ts'; @@ -62,62 +61,23 @@ import {initRepoContributors} from './features/contributors.ts'; import {initRepoCodeFrequency} from './features/code-frequency.ts'; import {initRepoRecentCommits} from './features/recent-commits.ts'; import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.ts'; -import {initAddedElementObserver} from './modules/observer.ts'; +import {initGlobalSelectorObserver} from './modules/observer.ts'; import {initRepositorySearch} from './features/repo-search.ts'; import {initColorPickers} from './features/colorpicker.ts'; import {initAdminSelfCheck} from './features/admin/selfcheck.ts'; import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts'; import {initGlobalFetchAction} from './features/common-fetch-action.ts'; -import { - initFootLanguageMenu, - initGlobalDropdown, - initGlobalTabularMenu, - initHeadNavbarContentToggle, -} from './features/common-page.ts'; -import { - initGlobalButtonClickOnEnter, - initGlobalButtons, - initGlobalDeleteButton, -} from './features/common-button.ts'; -import { - initGlobalComboMarkdownEditor, - initGlobalEnterQuickSubmit, - initGlobalFormDirtyLeaveConfirm, -} from './features/common-form.ts'; +import {initFootLanguageMenu, initGlobalDropdown, initGlobalInput, initGlobalTabularMenu, initHeadNavbarContentToggle} from './features/common-page.ts'; +import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} from './features/common-button.ts'; +import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts'; +import {callInitFunctions} from './modules/init.ts'; initGiteaFomantic(); -initAddedElementObserver(); initSubmitEventPolyfill(); -function callInitFunctions(functions: (() => any)[]) { - // Start performance trace by accessing a URL by "https://localhost/?_ui_performance_trace=1" or "https://localhost/?key=value&_ui_performance_trace=1" - // It is a quick check, no side effect so no need to do slow URL parsing. - const initStart = performance.now(); - if (window.location.search.includes('_ui_performance_trace=1')) { - let results: {name: string, dur: number}[] = []; - for (const func of functions) { - const start = performance.now(); - func(); - results.push({name: func.name, dur: performance.now() - start}); - } - results = results.sort((a, b) => b.dur - a.dur); - for (let i = 0; i < 20 && i < results.length; i++) { - // eslint-disable-next-line no-console - console.log(`performance trace: ${results[i].name} ${results[i].dur.toFixed(3)}`); - } - } else { - for (const func of functions) { - func(); - } - } - const initDur = performance.now() - initStart; - if (initDur > 500) { - console.error(`slow init functions took ${initDur.toFixed(3)}ms`); - } -} - onDomReady(() => { - callInitFunctions([ + const initStartTime = performance.now(); + const initPerformanceTracer = callInitFunctions([ initGlobalDropdown, initGlobalTabularMenu, initGlobalFetchAction, @@ -129,6 +89,7 @@ onDomReady(() => { initGlobalFormDirtyLeaveConfirm, initGlobalComboMarkdownEditor, initGlobalDeleteButton, + initGlobalInput, initCommonOrganization, initCommonIssueListQuickGoto, @@ -150,7 +111,6 @@ onDomReady(() => { initSshKeyFormParser, initStopwatch, initTableSort, - initAutoFocusEnd, initFindFileInRepo, initCopyContent, @@ -212,4 +172,13 @@ onDomReady(() => { initOAuth2SettingsDisableCheckbox, ]); + + // it must be the last one, then the "querySelectorAll" only needs to be executed once for global init functions. + initGlobalSelectorObserver(initPerformanceTracer); + if (initPerformanceTracer) initPerformanceTracer.printResults(); + + const initDur = performance.now() - initStartTime; + if (initDur > 500) { + console.error(`slow init functions took ${initDur.toFixed(3)}ms`); + } }); diff --git a/web_src/js/modules/init.ts b/web_src/js/modules/init.ts new file mode 100644 index 0000000000..538fafd83f --- /dev/null +++ b/web_src/js/modules/init.ts @@ -0,0 +1,26 @@ +export class InitPerformanceTracer { + results: {name: string, dur: number}[] = []; + recordCall(name: string, func: ()=>void) { + const start = performance.now(); + func(); + this.results.push({name, dur: performance.now() - start}); + } + printResults() { + this.results = this.results.sort((a, b) => b.dur - a.dur); + for (let i = 0; i < 20 && i < this.results.length; i++) { + console.info(`performance trace: ${this.results[i].name} ${this.results[i].dur.toFixed(3)}`); + } + } +} + +export function callInitFunctions(functions: (() => any)[]): InitPerformanceTracer | null { + // Start performance trace by accessing a URL by "https://localhost/?_ui_performance_trace=1" or "https://localhost/?key=value&_ui_performance_trace=1" + // It is a quick check, no side effect so no need to do slow URL parsing. + const perfTracer = !window.location.search.includes('_ui_performance_trace=1') ? null : new InitPerformanceTracer(); + if (perfTracer) { + for (const func of functions) perfTracer.recordCall(func.name, func); + } else { + for (const func of functions) func(); + } + return perfTracer; +} diff --git a/web_src/js/modules/observer.ts b/web_src/js/modules/observer.ts index 57d0db31c7..f60c033cf2 100644 --- a/web_src/js/modules/observer.ts +++ b/web_src/js/modules/observer.ts @@ -1,52 +1,73 @@ import {isDocumentFragmentOrElementNode} from '../utils/dom.ts'; +import type {Promisable} from 'type-fest'; +import type {InitPerformanceTracer} from './init.ts'; -type DirElement = HTMLInputElement | HTMLTextAreaElement; +let globalSelectorObserverInited = false; -// for performance considerations, it only uses performant syntax -function attachDirAuto(el: Partial<DirElement>) { - if (el.type !== 'hidden' && - el.type !== 'checkbox' && - el.type !== 'radio' && - el.type !== 'range' && - el.type !== 'color') { - el.dir = 'auto'; +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, ...) }); +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); } } -type GlobalInitFunc<T extends HTMLElement> = (el: T) => void | Promise<void>; -const globalInitFuncs: Record<string, GlobalInitFunc<HTMLElement>> = {}; -function attachGlobalInit(el: HTMLElement) { +// 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) { const initFunc = el.getAttribute('data-global-init'); const func = globalInitFuncs[initFunc]; if (!func) throw new Error(`Global init function "${initFunc}" not found`); + + type GiteaGlobalInitElement = Partial<HTMLElement> & {_giteaGlobalInited: boolean}; + if ((el as GiteaGlobalInitElement)._giteaGlobalInited) throw new Error(`Global init function "${initFunc}" already executed`); + (el as GiteaGlobalInitElement)._giteaGlobalInited = true; func(el); } -type GlobalEventFunc<T extends HTMLElement, E extends Event> = (el: T, e: E) => (void | Promise<void>); -const globalEventFuncs: Record<string, GlobalEventFunc<HTMLElement, Event>> = {}; -export function registerGlobalEventFunc<T extends HTMLElement, E extends Event>(event: string, name: string, func: GlobalEventFunc<T, E>) { - globalEventFuncs[`${event}:${name}`] = func as any; +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); + }); } -type SelectorHandler = { - selector: string, - handler: (el: HTMLElement) => void, -}; +export function initGlobalSelectorObserver(perfTracer?: InitPerformanceTracer): void { + if (globalSelectorObserverInited) throw new Error('initGlobalSelectorObserver() already called'); + globalSelectorObserverInited = true; -const selectorHandlers: SelectorHandler[] = [ - {selector: 'input, textarea', handler: attachDirAuto}, - {selector: '[data-global-init]', handler: attachGlobalInit}, -]; + attachGlobalEvents(); -export function observeAddedElement(selector: string, handler: (el: HTMLElement) => void) { - selectorHandlers.push({selector, handler}); - const docNodes = document.querySelectorAll<HTMLElement>(selector); - for (const el of docNodes) { - handler(el); - } -} - -export function initAddedElementObserver(): void { + selectorHandlers.push({selector: '[data-global-init]', handler: callGlobalInitFunc}); const observer = new MutationObserver((mutationList) => { const len = mutationList.length; for (let i = 0; i < len; i++) { @@ -60,30 +81,27 @@ export function initAddedElementObserver(): void { if (addedNode.matches(selector)) { handler(addedNode); } - const children = addedNode.querySelectorAll<HTMLElement>(selector); - for (const el of children) { + for (const el of addedNode.querySelectorAll<HTMLElement>(selector)) { handler(el); } } } } }); - - for (const {selector, handler} of selectorHandlers) { - const docNodes = document.querySelectorAll<HTMLElement>(selector); - for (const el of docNodes) { - handler(el); + 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); + } } } - observer.observe(document, {subtree: true, childList: true}); - - 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); - }); } diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 603f967b34..4d15784e6e 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -355,7 +355,7 @@ export function querySingleVisibleElem<T extends HTMLElement>(parent: Element, s return candidates.length ? candidates[0] as T : null; } -export function addDelegatedEventListener<T extends HTMLElement, E extends Event>(parent: Node, type: string, selector: string, listener: (elem: T, e: E) => void | Promise<any>, options?: boolean | AddEventListenerOptions) { +export function addDelegatedEventListener<T extends HTMLElement, E extends Event>(parent: Node, type: string, selector: string, listener: (elem: T, e: E) => Promisable<void>, options?: boolean | AddEventListenerOptions) { parent.addEventListener(type, (e: Event) => { const elem = (e.target as HTMLElement).closest(selector); if (!elem) return; |