mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	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.
This commit is contained in:
		| @@ -62,6 +62,10 @@ func TestSanitizer(t *testing.T) { | |||||||
| 		`<a href="javascript:alert('xss')">bad</a>`, `bad`, | 		`<a href="javascript:alert('xss')">bad</a>`, `bad`, | ||||||
| 		`<a href="vbscript:no">bad</a>`, `bad`, | 		`<a href="vbscript:no">bad</a>`, `bad`, | ||||||
| 		`<a href="data:1234">bad</a>`, `bad`, | 		`<a href="data:1234">bad</a>`, `bad`, | ||||||
|  |  | ||||||
|  | 		// Some classes and attributes are used by the frontend framework and will execute JS code, so make sure they are removed | ||||||
|  | 		`<div class="link-action" data-attr-class="foo" data-url="xxx">txt</div>`, `<div data-attr-class="foo">txt</div>`, | ||||||
|  | 		`<div class="form-fetch-action" data-markdown-generated-content="bar" data-global-init="a" data-global-click="b">txt</div>`, `<div data-markdown-generated-content="bar">txt</div>`, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for i := 0; i < len(testCases); i += 2 { | 	for i := 0; i < len(testCases); i += 2 { | ||||||
|   | |||||||
| @@ -1,20 +1,20 @@ | |||||||
| {{if .Flash.ErrorMsg}} | {{- if .Flash.ErrorMsg -}} | ||||||
| 	<div class="ui negative message flash-message flash-error"> | 	<div class="ui negative message flash-message flash-error"> | ||||||
| 		<p>{{.Flash.ErrorMsg | SanitizeHTML}}</p> | 		<p>{{.Flash.ErrorMsg | SanitizeHTML}}</p> | ||||||
| 	</div> | 	</div> | ||||||
| {{end}} | {{- end -}} | ||||||
| {{if .Flash.SuccessMsg}} | {{- if .Flash.SuccessMsg -}} | ||||||
| 	<div class="ui positive message flash-message flash-success"> | 	<div class="ui positive message flash-message flash-success"> | ||||||
| 		<p>{{.Flash.SuccessMsg | SanitizeHTML}}</p> | 		<p>{{.Flash.SuccessMsg | SanitizeHTML}}</p> | ||||||
| 	</div> | 	</div> | ||||||
| {{end}} | {{- end -}} | ||||||
| {{if .Flash.InfoMsg}} | {{- if .Flash.InfoMsg -}} | ||||||
| 	<div class="ui info message flash-message flash-info"> | 	<div class="ui info message flash-message flash-info"> | ||||||
| 		<p>{{.Flash.InfoMsg | SanitizeHTML}}</p> | 		<p>{{.Flash.InfoMsg | SanitizeHTML}}</p> | ||||||
| 	</div> | 	</div> | ||||||
| {{end}} | {{- end -}} | ||||||
| {{if .Flash.WarningMsg}} | {{- if .Flash.WarningMsg -}} | ||||||
| 	<div class="ui warning message flash-message flash-warning"> | 	<div class="ui warning message flash-message flash-warning"> | ||||||
| 		<p>{{.Flash.WarningMsg | SanitizeHTML}}</p> | 		<p>{{.Flash.WarningMsg | SanitizeHTML}}</p> | ||||||
| 	</div> | 	</div> | ||||||
| {{end}} | {{- end -}} | ||||||
|   | |||||||
| @@ -1,6 +1,4 @@ | |||||||
| {{if .Flash}} |  | ||||||
| {{template "base/alert" .}} | {{template "base/alert" .}} | ||||||
| {{end}} |  | ||||||
| <form class="issue-content ui comment form form-fetch-action" id="new-issue" action="{{.Link}}" method="post"> | <form class="issue-content ui comment form form-fetch-action" id="new-issue" action="{{.Link}}" method="post"> | ||||||
| 	{{.CsrfTokenHtml}} | 	{{.CsrfTokenHtml}} | ||||||
| 	<div class="issue-content-left"> | 	<div class="issue-content-left"> | ||||||
| @@ -9,7 +7,10 @@ | |||||||
| 				{{ctx.AvatarUtils.Avatar .SignedUser 40}} | 				{{ctx.AvatarUtils.Avatar .SignedUser 40}} | ||||||
| 				<div class="ui segment content tw-my-0"> | 				<div class="ui segment content tw-my-0"> | ||||||
| 					<div class="field"> | 					<div class="field"> | ||||||
| 						<input name="title" class="js-autofocus-end" id="issue_title" placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" required maxlength="255" autocomplete="off"> | 						<input name="title" data-global-init="initInputAutoFocusEnd" id="issue_title" required maxlength="255" autocomplete="off" | ||||||
|  | 								placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}" | ||||||
|  | 								value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" | ||||||
|  | 						> | ||||||
| 						{{if .PageIsComparePull}} | 						{{if .PageIsComparePull}} | ||||||
| 							<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0)}}</div> | 							<div class="title_wip_desc" data-wip-prefixes="{{JsonUtils.EncodeToString .PullRequestWorkInProgressPrefixes}}">{{ctx.Locale.Tr "repo.pulls.title_wip_desc" (index .PullRequestWorkInProgressPrefixes 0)}}</div> | ||||||
| 						{{end}} | 						{{end}} | ||||||
|   | |||||||
| @@ -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); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -2,7 +2,7 @@ import {GET} from '../modules/fetch.ts'; | |||||||
| import {showGlobalErrorMessage} from '../bootstrap.ts'; | import {showGlobalErrorMessage} from '../bootstrap.ts'; | ||||||
| import {fomanticQuery} from '../modules/fomantic/base.ts'; | import {fomanticQuery} from '../modules/fomantic/base.ts'; | ||||||
| import {queryElems} from '../utils/dom.ts'; | import {queryElems} from '../utils/dom.ts'; | ||||||
| import {observeAddedElement} from '../modules/observer.ts'; | import {registerGlobalInitFunc, registerGlobalSelectorFunc} from '../modules/observer.ts'; | ||||||
|  |  | ||||||
| const {appUrl} = window.config; | const {appUrl} = window.config; | ||||||
|  |  | ||||||
| @@ -30,7 +30,7 @@ export function initFootLanguageMenu() { | |||||||
|  |  | ||||||
| export function initGlobalDropdown() { | export function initGlobalDropdown() { | ||||||
|   // do not init "custom" dropdowns, "custom" dropdowns are managed by their own code. |   // 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); |     const $dropdown = fomanticQuery(el); | ||||||
|     if ($dropdown.data('module-dropdown')) return; // do not re-init if other code has already initialized it. |     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}); |   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: |  * Too many users set their ROOT_URL to wrong value, and it causes a lot of problems: | ||||||
|  *   * Cross-origin API request without correct cookie |  *   * Cross-origin API request without correct cookie | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ import {POST, GET} from '../modules/fetch.ts'; | |||||||
| import {createTippy} from '../modules/tippy.ts'; | import {createTippy} from '../modules/tippy.ts'; | ||||||
| import {invertFileFolding} from './file-fold.ts'; | import {invertFileFolding} from './file-fold.ts'; | ||||||
| import {parseDom} from '../utils.ts'; | import {parseDom} from '../utils.ts'; | ||||||
| import {observeAddedElement} from '../modules/observer.ts'; | import {registerGlobalSelectorFunc} from '../modules/observer.ts'; | ||||||
|  |  | ||||||
| const {i18n} = window.config; | const {i18n} = window.config; | ||||||
|  |  | ||||||
| @@ -254,7 +254,7 @@ export function initRepoDiffView() { | |||||||
|   initExpandAndCollapseFilesButton(); |   initExpandAndCollapseFilesButton(); | ||||||
|   initRepoDiffHashChangeListener(); |   initRepoDiffHashChangeListener(); | ||||||
|  |  | ||||||
|   observeAddedElement('#diff-file-boxes .diff-file-box', initRepoDiffFileBox); |   registerGlobalSelectorFunc('#diff-file-boxes .diff-file-box', initRepoDiffFileBox); | ||||||
|   addDelegatedEventListener(document, 'click', '.fold-file', (el) => { |   addDelegatedEventListener(document, 'click', '.fold-file', (el) => { | ||||||
|     invertFileFolding(el.closest('.file-content'), el); |     invertFileFolding(el.closest('.file-content'), el); | ||||||
|   }); |   }); | ||||||
|   | |||||||
| @@ -11,7 +11,6 @@ import {initImageDiff} from './features/imagediff.ts'; | |||||||
| import {initRepoMigration} from './features/repo-migration.ts'; | import {initRepoMigration} from './features/repo-migration.ts'; | ||||||
| import {initRepoProject} from './features/repo-projects.ts'; | import {initRepoProject} from './features/repo-projects.ts'; | ||||||
| import {initTableSort} from './features/tablesort.ts'; | import {initTableSort} from './features/tablesort.ts'; | ||||||
| import {initAutoFocusEnd} from './features/autofocus-end.ts'; |  | ||||||
| import {initAdminUserListSearchForm} from './features/admin/users.ts'; | import {initAdminUserListSearchForm} from './features/admin/users.ts'; | ||||||
| import {initAdminConfigs} from './features/admin/config.ts'; | import {initAdminConfigs} from './features/admin/config.ts'; | ||||||
| import {initMarkupAnchors} from './markup/anchors.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 {initRepoCodeFrequency} from './features/code-frequency.ts'; | ||||||
| import {initRepoRecentCommits} from './features/recent-commits.ts'; | import {initRepoRecentCommits} from './features/recent-commits.ts'; | ||||||
| import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.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 {initRepositorySearch} from './features/repo-search.ts'; | ||||||
| import {initColorPickers} from './features/colorpicker.ts'; | import {initColorPickers} from './features/colorpicker.ts'; | ||||||
| import {initAdminSelfCheck} from './features/admin/selfcheck.ts'; | import {initAdminSelfCheck} from './features/admin/selfcheck.ts'; | ||||||
| import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts'; | import {initOAuth2SettingsDisableCheckbox} from './features/oauth2-settings.ts'; | ||||||
| import {initGlobalFetchAction} from './features/common-fetch-action.ts'; | import {initGlobalFetchAction} from './features/common-fetch-action.ts'; | ||||||
| import { | import {initFootLanguageMenu, initGlobalDropdown, initGlobalInput, initGlobalTabularMenu, initHeadNavbarContentToggle} from './features/common-page.ts'; | ||||||
|   initFootLanguageMenu, | import {initGlobalButtonClickOnEnter, initGlobalButtons, initGlobalDeleteButton} from './features/common-button.ts'; | ||||||
|   initGlobalDropdown, | import {initGlobalComboMarkdownEditor, initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts'; | ||||||
|   initGlobalTabularMenu, | import {callInitFunctions} from './modules/init.ts'; | ||||||
|   initHeadNavbarContentToggle, |  | ||||||
| } from './features/common-page.ts'; |  | ||||||
| import { |  | ||||||
|   initGlobalButtonClickOnEnter, |  | ||||||
|   initGlobalButtons, |  | ||||||
|   initGlobalDeleteButton, |  | ||||||
| } from './features/common-button.ts'; |  | ||||||
| import { |  | ||||||
|   initGlobalComboMarkdownEditor, |  | ||||||
|   initGlobalEnterQuickSubmit, |  | ||||||
|   initGlobalFormDirtyLeaveConfirm, |  | ||||||
| } from './features/common-form.ts'; |  | ||||||
|  |  | ||||||
| initGiteaFomantic(); | initGiteaFomantic(); | ||||||
| initAddedElementObserver(); |  | ||||||
| initSubmitEventPolyfill(); | 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(() => { | onDomReady(() => { | ||||||
|   callInitFunctions([ |   const initStartTime = performance.now(); | ||||||
|  |   const initPerformanceTracer = callInitFunctions([ | ||||||
|     initGlobalDropdown, |     initGlobalDropdown, | ||||||
|     initGlobalTabularMenu, |     initGlobalTabularMenu, | ||||||
|     initGlobalFetchAction, |     initGlobalFetchAction, | ||||||
| @@ -129,6 +89,7 @@ onDomReady(() => { | |||||||
|     initGlobalFormDirtyLeaveConfirm, |     initGlobalFormDirtyLeaveConfirm, | ||||||
|     initGlobalComboMarkdownEditor, |     initGlobalComboMarkdownEditor, | ||||||
|     initGlobalDeleteButton, |     initGlobalDeleteButton, | ||||||
|  |     initGlobalInput, | ||||||
|  |  | ||||||
|     initCommonOrganization, |     initCommonOrganization, | ||||||
|     initCommonIssueListQuickGoto, |     initCommonIssueListQuickGoto, | ||||||
| @@ -150,7 +111,6 @@ onDomReady(() => { | |||||||
|     initSshKeyFormParser, |     initSshKeyFormParser, | ||||||
|     initStopwatch, |     initStopwatch, | ||||||
|     initTableSort, |     initTableSort, | ||||||
|     initAutoFocusEnd, |  | ||||||
|     initFindFileInRepo, |     initFindFileInRepo, | ||||||
|     initCopyContent, |     initCopyContent, | ||||||
|  |  | ||||||
| @@ -212,4 +172,13 @@ onDomReady(() => { | |||||||
|  |  | ||||||
|     initOAuth2SettingsDisableCheckbox, |     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`); | ||||||
|  |   } | ||||||
| }); | }); | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								web_src/js/modules/init.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								web_src/js/modules/init.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||||
|  | } | ||||||
| @@ -1,52 +1,73 @@ | |||||||
| import {isDocumentFragmentOrElementNode} from '../utils/dom.ts'; | 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 | type SelectorHandler = {selector: string, handler: (el: HTMLElement) => void}; | ||||||
| function attachDirAuto(el: Partial<DirElement>) { | const selectorHandlers: SelectorHandler[] = []; | ||||||
|   if (el.type !== 'hidden' && |  | ||||||
|       el.type !== 'checkbox' && |  | ||||||
|       el.type !== 'radio' && |  | ||||||
|       el.type !== 'range' && |  | ||||||
|       el.type !== 'color') { |  | ||||||
|     el.dir = 'auto'; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type GlobalInitFunc<T extends HTMLElement> = (el: T) => void | Promise<void>; | type GlobalEventFunc<T extends HTMLElement, E extends Event> = (el: T, e: E) => Promisable<void>; | ||||||
| const globalInitFuncs: Record<string, GlobalInitFunc<HTMLElement>> = {}; |  | ||||||
| function attachGlobalInit(el: HTMLElement) { |  | ||||||
|   const initFunc = el.getAttribute('data-global-init'); |  | ||||||
|   const func = globalInitFuncs[initFunc]; |  | ||||||
|   if (!func) throw new Error(`Global init function "${initFunc}" not found`); |  | ||||||
|   func(el); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type GlobalEventFunc<T extends HTMLElement, E extends Event> = (el: T, e: E) => (void | Promise<void>); |  | ||||||
| const globalEventFuncs: Record<string, GlobalEventFunc<HTMLElement, Event>> = {}; | 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>) { | export function registerGlobalEventFunc<T extends HTMLElement, E extends Event>(event: string, name: string, func: GlobalEventFunc<T, E>) { | ||||||
|   globalEventFuncs[`${event}:${name}`] = func as any; |   globalEventFuncs[`${event}:${name}`] = func as GlobalEventFunc<HTMLElement, Event>; | ||||||
| } | } | ||||||
|  |  | ||||||
| type SelectorHandler = { | // It handles the global init functions by a selector, for example: | ||||||
|   selector: string, | // > registerGlobalSelectorObserver('.ui.dropdown:not(.custom)', (el) => { initDropdown(el, ...) }); | ||||||
|   handler: (el: HTMLElement) => void, | export function registerGlobalSelectorFunc(selector: string, handler: (el: HTMLElement) => void) { | ||||||
| }; |  | ||||||
|  |  | ||||||
| const selectorHandlers: SelectorHandler[] = [ |  | ||||||
|   {selector: 'input, textarea', handler: attachDirAuto}, |  | ||||||
|   {selector: '[data-global-init]', handler: attachGlobalInit}, |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| export function observeAddedElement(selector: string, handler: (el: HTMLElement) => void) { |  | ||||||
|   selectorHandlers.push({selector, handler}); |   selectorHandlers.push({selector, handler}); | ||||||
|   const docNodes = document.querySelectorAll<HTMLElement>(selector); |   // Then initAddedElementObserver will call this handler for all existing elements after all handlers are added. | ||||||
|   for (const el of docNodes) { |   // 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); |     handler(el); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| export function initAddedElementObserver(): void { | // 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); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function initGlobalSelectorObserver(perfTracer?: InitPerformanceTracer): void { | ||||||
|  |   if (globalSelectorObserverInited) throw new Error('initGlobalSelectorObserver() already called'); | ||||||
|  |   globalSelectorObserverInited = true; | ||||||
|  |  | ||||||
|  |   attachGlobalEvents(); | ||||||
|  |  | ||||||
|  |   selectorHandlers.push({selector: '[data-global-init]', handler: callGlobalInitFunc}); | ||||||
|   const observer = new MutationObserver((mutationList) => { |   const observer = new MutationObserver((mutationList) => { | ||||||
|     const len = mutationList.length; |     const len = mutationList.length; | ||||||
|     for (let i = 0; i < len; i++) { |     for (let i = 0; i < len; i++) { | ||||||
| @@ -60,30 +81,27 @@ export function initAddedElementObserver(): void { | |||||||
|           if (addedNode.matches(selector)) { |           if (addedNode.matches(selector)) { | ||||||
|             handler(addedNode); |             handler(addedNode); | ||||||
|           } |           } | ||||||
|           const children = addedNode.querySelectorAll<HTMLElement>(selector); |           for (const el of addedNode.querySelectorAll<HTMLElement>(selector)) { | ||||||
|           for (const el of children) { |  | ||||||
|             handler(el); |             handler(el); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|  |   if (perfTracer) { | ||||||
|     for (const {selector, handler} of selectorHandlers) { |     for (const {selector, handler} of selectorHandlers) { | ||||||
|     const docNodes = document.querySelectorAll<HTMLElement>(selector); |       perfTracer.recordCall(`initGlobalSelectorObserver ${selector}`, () => { | ||||||
|     for (const el of docNodes) { |         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); |         handler(el); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |   } | ||||||
|   observer.observe(document, {subtree: true, childList: true}); |   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); |  | ||||||
|   }); |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -355,7 +355,7 @@ export function querySingleVisibleElem<T extends HTMLElement>(parent: Element, s | |||||||
|   return candidates.length ? candidates[0] as T : null; |   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) => { |   parent.addEventListener(type, (e: Event) => { | ||||||
|     const elem = (e.target as HTMLElement).closest(selector); |     const elem = (e.target as HTMLElement).closest(selector); | ||||||
|     if (!elem) return; |     if (!elem) return; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user