aboutsummaryrefslogtreecommitdiffstats
path: root/web_src/js
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2025-03-03 10:57:28 +0800
committerGitHub <noreply@github.com>2025-03-03 10:57:28 +0800
commit27bf63ad20e3241d48636064f585a6d432bdcaa7 (patch)
treeab18faca2e931374b628d9ff4c09aa9d151411ae /web_src/js
parent5cbdf83f706ecdace65e5b42897ebbde82c3a0a1 (diff)
downloadgitea-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.ts6
-rw-r--r--web_src/js/features/common-page.ts23
-rw-r--r--web_src/js/features/repo-diff.ts4
-rw-r--r--web_src/js/index.ts65
-rw-r--r--web_src/js/modules/init.ts26
-rw-r--r--web_src/js/modules/observer.ts118
-rw-r--r--web_src/js/utils/dom.ts2
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;