mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
chore: refactor legacy JS init code to use "data-global-init" (#39836)
Move the remaining legacy init code, which probed page classes with `querySelector`, to `data-global-init`, so each feature initializes from its own element. `hashchange` listeners now go through one module that runs them in priority order, because independent listeners broke when mount order changed. - Convert issue list, diff, repo create, settings and org team inits to `data-global-init` - Run `hashchange` listeners by priority, the diff scroll handler last - Make the devtest mock's failing log requests deterministic - Add e2e tests for diff tree navigation to folded files and job log permalinks --------- Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
1 parent
ae17c14cff
commit
f690ea8efd
26 files changed
+122
-125
No files matched your search
@@ -15,6 +15,7 @@ import type {ActionsArtifact, ActionsJob, ActionsRun, ActionsStatus} from '../mo
|
||||
import {AnsiLineRenderer} from '../render/ansi.ts';
|
||||
import {createLogLineMessage, parseLogLineCommand} from './ActionRunView.ts';
|
||||
import type {ActionRunViewStore, LogLine, LogLineCommand} from './ActionRunView.ts';
|
||||
import {registerAsyncHashChangeListener, removeAsyncHashChangeListener} from '../modules/hashchange.ts';
|
||||
|
||||
function isLogElementInViewport(el: Element, {extraViewPortHeight} = {extraViewPortHeight: 0}): boolean {
|
||||
const rect = el.getBoundingClientRect();
|
||||
@@ -176,13 +177,12 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
intervalID = setInterval(() => loadJob(), 1000);
|
||||
hashChangeListener();
|
||||
window.addEventListener('hashchange', hashChangeListener);
|
||||
registerAsyncHashChangeListener(hashChangeListener);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
menuTippy.destroy();
|
||||
window.removeEventListener('hashchange', hashChangeListener);
|
||||
removeAsyncHashChangeListener(hashChangeListener);
|
||||
// clear the interval timer when the component is unmounted
|
||||
// even our page is rendered once, not spa style
|
||||
if (intervalID) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {diffTreeStore, filterDiffTree, applyFiltersToFileBoxes, extensionFilterT
|
||||
import {setFileFolding} from '../features/file-fold.ts';
|
||||
import {onMounted, onUnmounted, computed, watch} from 'vue';
|
||||
import {localUserSettings} from '../modules/user-settings.ts';
|
||||
import {registerAsyncHashChangeListener, removeAsyncHashChangeListener} from '../modules/hashchange.ts';
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'diff_file_tree_visible';
|
||||
|
||||
@@ -28,13 +29,12 @@ onMounted(() => {
|
||||
// while the tree is hidden there is no control to clear a filter restored from the URL
|
||||
if (store.fileTreeIsVisible) applyFiltersToFileBoxes(store); else store.activeExtensions = 'all';
|
||||
document.querySelector('.diff-toggle-file-tree-button')!.addEventListener('click', toggleVisibility);
|
||||
hashChangeListener();
|
||||
window.addEventListener('hashchange', hashChangeListener);
|
||||
registerAsyncHashChangeListener(hashChangeListener);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.querySelector('.diff-toggle-file-tree-button')!.removeEventListener('click', toggleVisibility);
|
||||
window.removeEventListener('hashchange', hashChangeListener);
|
||||
removeAsyncHashChangeListener(hashChangeListener);
|
||||
});
|
||||
|
||||
function hashChangeListener() {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {queryElems, toggleElem} from '../utils/dom.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
|
||||
function initOrgTeamSettings() {
|
||||
// on the page "page-content organization new team"
|
||||
const pageContent = document.querySelector('.page-content.organization.new.team');
|
||||
if (!pageContent) return;
|
||||
function initOrgTeamSettings(pageContent: HTMLElement) {
|
||||
queryElems(pageContent, 'input[name=permission]', (el) => el.addEventListener('change', () => {
|
||||
// Change team access mode
|
||||
const val = pageContent.querySelector<HTMLInputElement>('input[name=permission]:checked')?.value;
|
||||
@@ -12,6 +10,5 @@ function initOrgTeamSettings() {
|
||||
}
|
||||
|
||||
export function initOrgTeam() {
|
||||
if (!document.querySelector('.page-content.organization')) return;
|
||||
initOrgTeamSettings();
|
||||
registerGlobalInitFunc('initOrgTeamSettings', initOrgTeamSettings);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {svg} from '../svg.ts';
|
||||
import {createTippy} from '../modules/tippy.ts';
|
||||
import {addDelegatedEventListener} from '../utils/dom.ts';
|
||||
import {registerAsyncHashChangeListener} from '../modules/hashchange.ts';
|
||||
|
||||
function changeHash(hash: string) {
|
||||
if (window.history.pushState) {
|
||||
@@ -142,6 +143,5 @@ export function initRepoCodeView() {
|
||||
showLineButton();
|
||||
}
|
||||
};
|
||||
onHashChange();
|
||||
window.addEventListener('hashchange', onHashChange);
|
||||
registerAsyncHashChangeListener(onHashChange);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import {createApp} from 'vue';
|
||||
import DiffCommitSelector from '../components/DiffCommitSelector.vue';
|
||||
|
||||
export function initDiffCommitSelect() {
|
||||
const el = document.querySelector('#diff-commit-select');
|
||||
if (!el) return;
|
||||
|
||||
const commitSelect = createApp(DiffCommitSelector);
|
||||
commitSelect.mount(el);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import {createApp} from 'vue';
|
||||
import DiffFileTree from '../components/DiffFileTree.vue';
|
||||
import type {DiffFileTreeLocale} from '../modules/diff-file.ts';
|
||||
|
||||
export function initDiffFileTree() {
|
||||
const el = document.querySelector('#diff-file-tree');
|
||||
if (!el) return;
|
||||
|
||||
const locale = JSON.parse(el.getAttribute('data-locale')!) as DiffFileTreeLocale;
|
||||
createApp(DiffFileTree, {locale}).mount(el);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import {createApp} from 'vue';
|
||||
import DiffFileTree from '../components/DiffFileTree.vue';
|
||||
import {initRepoIssueContentHistory} from './repo-issue-content.ts';
|
||||
import {initDiffFileTree} from './repo-diff-filetree.ts';
|
||||
import {initDiffCommitSelect} from './repo-diff-commitselect.ts';
|
||||
import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.ts';
|
||||
import {initExpandAndCollapseFilesButton, initDiffFileViewedForm} from './pull-view-file.ts';
|
||||
import {showErrorToast} from '../modules/toast.ts';
|
||||
@@ -12,8 +12,10 @@ import {invertFileFolding} from './file-fold.ts';
|
||||
import {parseDom} from '../utils.ts';
|
||||
import {registerGlobalEventFunc, registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import {performFetchActionTrigger} from '../modules/fetch-action.ts';
|
||||
import {applyFiltersToFileBoxes, diffTreeStore} from '../modules/diff-file.ts';
|
||||
import {applyFiltersToFileBoxes, type DiffFileTreeLocale, diffTreeStore} from '../modules/diff-file.ts';
|
||||
import {initImageDiff} from './imagediff.ts';
|
||||
import DiffCommitSelector from '../components/DiffCommitSelector.vue';
|
||||
import {registerAsyncHashChangeListener} from '../modules/hashchange.ts';
|
||||
|
||||
function initDiffFileViewToggle(el: HTMLElement) {
|
||||
// switch between "rendered" and "source", for image and CSV files
|
||||
@@ -245,6 +247,15 @@ async function onLocationHashChange() {
|
||||
}
|
||||
}
|
||||
|
||||
function initDiffFileTree(el: HTMLElement) {
|
||||
const locale = JSON.parse(el.getAttribute('data-locale')!) as DiffFileTreeLocale;
|
||||
createApp(DiffFileTree, {locale}).mount(el);
|
||||
}
|
||||
|
||||
function initDiffCommitSelect(el: HTMLElement) {
|
||||
createApp(DiffCommitSelector).mount(el);
|
||||
}
|
||||
|
||||
export function initRepoDiffView() {
|
||||
initRepoDiffConversationForm(); // such form appears on the "conversation" page and "diff" page
|
||||
registerGlobalEventFunc('click', 'diffLoadMoreFiles', (el) => { diffLoadMoreFiles(el) });
|
||||
@@ -254,13 +265,14 @@ export function initRepoDiffView() {
|
||||
registerGlobalInitFunc('initDiffFileViewedForm', initDiffFileViewedForm);
|
||||
registerGlobalInitFunc('initDiffFileImageDiff', initImageDiff);
|
||||
registerGlobalInitFunc('initDiffFileViewToggle', initDiffFileViewToggle);
|
||||
registerGlobalInitFunc('initDiffFileTree', initDiffFileTree);
|
||||
registerGlobalInitFunc('initDiffCommitSelect', initDiffCommitSelect);
|
||||
|
||||
if (!document.querySelector('#diff-file-boxes')) return;
|
||||
initRepoDiffConversationNav(); // "previous" and "next" buttons only appear on "diff" page
|
||||
initDiffFileTree();
|
||||
initDiffCommitSelect();
|
||||
initExpandAndCollapseFilesButton();
|
||||
|
||||
window.addEventListener('hashchange', onLocationHashChange);
|
||||
onLocationHashChange();
|
||||
// onLocationHashChange does very complicated things (e.g.: load incomplete content and scroll), and it might change the URL hash
|
||||
// so make it run after all other hash change listeners
|
||||
registerAsyncHashChangeListener(onLocationHashChange, 999);
|
||||
}
|
||||
@@ -1,42 +1,35 @@
|
||||
import {toggleElem, queryElems, isElemVisible} from '../utils/dom.ts';
|
||||
import {toggleElem, queryElems} from '../utils/dom.ts';
|
||||
import {html, htmlRaw} from '../utils/html.ts';
|
||||
import {confirmModal} from './comp/ConfirmModal.ts';
|
||||
import {createSortable} from '../modules/sortable.ts';
|
||||
import {DELETE, POST} from '../modules/fetch.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {performFetchAction} from '../modules/fetch-action.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
import type {SortableEvent} from 'sortablejs';
|
||||
|
||||
type IssuePoster = {avatar_link: string, full_name: string, username: string};
|
||||
type ProcessedIssuePoster = {type: 'html', html: string};
|
||||
type IssuePosterResponse = {results: IssuePoster[]};
|
||||
|
||||
function initRepoIssueListCheckboxes() {
|
||||
const issueSelectAll = document.querySelector<HTMLInputElement>('.issue-checkbox-all');
|
||||
if (!issueSelectAll) return; // logged out state
|
||||
const issueCheckboxes = document.querySelectorAll<HTMLInputElement>('.issue-checkbox');
|
||||
function initRepoIssueListCheckboxes(issueSelectAll: HTMLInputElement) {
|
||||
const container = issueSelectAll.closest('.page-content')!;
|
||||
const issueCheckboxes = container.querySelectorAll<HTMLInputElement>('.issue-checkbox');
|
||||
const issueFilters = container.querySelector<HTMLElement>('#issue-filters')!;
|
||||
const issueActions = container.querySelector<HTMLElement>('#issue-actions')!;
|
||||
|
||||
const syncIssueSelectionState = () => {
|
||||
const checkedCheckboxes = Array.from(issueCheckboxes).filter((el) => el.checked);
|
||||
const anyChecked = Boolean(checkedCheckboxes.length);
|
||||
const allChecked = anyChecked && checkedCheckboxes.length === issueCheckboxes.length;
|
||||
|
||||
if (allChecked) {
|
||||
issueSelectAll.checked = true;
|
||||
issueSelectAll.indeterminate = false;
|
||||
} else if (anyChecked) {
|
||||
issueSelectAll.checked = false;
|
||||
issueSelectAll.indeterminate = true;
|
||||
} else {
|
||||
issueSelectAll.checked = false;
|
||||
issueSelectAll.indeterminate = false;
|
||||
}
|
||||
issueSelectAll.checked = allChecked;
|
||||
issueSelectAll.indeterminate = anyChecked && !allChecked;
|
||||
// if any issue is selected, show the action panel, otherwise show the filter panel
|
||||
toggleElem('#issue-filters', !anyChecked);
|
||||
toggleElem('#issue-actions', anyChecked);
|
||||
toggleElem(issueFilters, !anyChecked);
|
||||
toggleElem(issueActions, anyChecked);
|
||||
// there are two panels but only one select-all checkbox, so move the checkbox to the visible panel
|
||||
const panels = document.querySelectorAll<HTMLElement>('#issue-filters, #issue-actions');
|
||||
const visiblePanel = Array.from(panels).find((el) => isElemVisible(el))!;
|
||||
const visiblePanel = anyChecked ? issueActions : issueFilters;
|
||||
const toolbarLeft = visiblePanel.querySelector('.issue-list-toolbar-left')!;
|
||||
toolbarLeft.prepend(issueSelectAll);
|
||||
};
|
||||
@@ -52,15 +45,14 @@ function initRepoIssueListCheckboxes() {
|
||||
syncIssueSelectionState();
|
||||
});
|
||||
|
||||
queryElems(document, '.issue-action', (el) => el.addEventListener('click',
|
||||
queryElems(container, '.issue-action', (el) => el.addEventListener('click',
|
||||
async (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const url = el.getAttribute('data-url')!;
|
||||
let action = el.getAttribute('data-action')!;
|
||||
const elementId = el.getAttribute('data-element-id')!;
|
||||
const issueIDList: string[] = Array.from(document.querySelectorAll('.issue-checkbox:checked'), (el) => (el.getAttribute('data-issue-id')!));
|
||||
const issueIDs = issueIDList.join(',');
|
||||
const issueIDs = Array.from(container.querySelectorAll('.issue-checkbox:checked'), (el) => el.getAttribute('data-issue-id')!).join(',');
|
||||
if (!issueIDs) return;
|
||||
|
||||
// for label toggle
|
||||
@@ -166,14 +158,7 @@ async function pinMoveEnd(e: SortableEvent) {
|
||||
await POST(url, {data: {id, position: e.newIndex! + 1}});
|
||||
}
|
||||
|
||||
function initIssuePinSort() {
|
||||
const pinDiv = document.querySelector<HTMLElement>('#issue-pins');
|
||||
|
||||
if (pinDiv === null) return;
|
||||
|
||||
// If the User is not a Repo Admin, we don't need to proceed
|
||||
if (!pinDiv.hasAttribute('data-is-repo-admin')) return;
|
||||
|
||||
function initIssuePinSort(pinDiv: HTMLElement) {
|
||||
initPinRemoveButton();
|
||||
|
||||
// If only one issue pinned, we don't need to make this Sortable
|
||||
@@ -190,12 +175,7 @@ function initIssuePinSort() {
|
||||
}
|
||||
|
||||
export function initRepoIssueList() {
|
||||
if (document.querySelector('.page-content.repository.issue-list, .page-content.repository.milestone-issue-list')) {
|
||||
initRepoIssueListCheckboxes();
|
||||
queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
|
||||
initIssuePinSort();
|
||||
} else if (document.querySelector('.page-content.dashboard.issues')) {
|
||||
// user or org home: issue list, pull request list
|
||||
queryElems(document, '.ui.dropdown.user-remote-search', (el) => initDropdownUserRemoteSearch(el));
|
||||
}
|
||||
registerGlobalInitFunc('initRepoIssueListCheckboxes', initRepoIssueListCheckboxes);
|
||||
registerGlobalInitFunc('initDropdownUserRemoteSearch', initDropdownUserRemoteSearch);
|
||||
registerGlobalInitFunc('initIssuePinSort', initIssuePinSort);
|
||||
}
|
||||
@@ -41,6 +41,7 @@ export function initRepository() {
|
||||
registerGlobalInitFunc('initRepoPullMergeBox', initRepoPullMergeBox);
|
||||
registerGlobalInitFunc('initRepoPullRequestUpdate', initRepoPullRequestUpdate);
|
||||
registerGlobalInitFunc('initRepoCitationPanel', initRepoCitationPanel);
|
||||
registerGlobalInitFunc('initRepoNew', initRepoNew);
|
||||
|
||||
// TODO: need to refactor more legacy if-querySelector-based "init" code to "data-global-xxx" framework in the future.
|
||||
const pageContent = document.querySelector('.page-content.repository');
|
||||
@@ -51,7 +52,6 @@ export function initRepository() {
|
||||
initRepoCommentForm();
|
||||
|
||||
initRepoMilestone();
|
||||
initRepoNew();
|
||||
|
||||
initRepoCloneButtons();
|
||||
initRepoSettings();
|
||||
|
||||
@@ -56,11 +56,7 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) {
|
||||
onChangeOwner();
|
||||
}
|
||||
|
||||
export function initRepoNew() {
|
||||
const pageContent = document.querySelector('.page-content.repository.new-repo');
|
||||
if (!pageContent) return;
|
||||
|
||||
const form = document.querySelector<HTMLFormElement>('.new-repo-form')!;
|
||||
export function initRepoNew(form: HTMLFormElement) {
|
||||
const inputGitIgnores = form.querySelector<HTMLInputElement>('input[name="gitignores"]')!;
|
||||
const inputLicense = form.querySelector<HTMLInputElement>('input[name="license"]')!;
|
||||
const inputAutoInit = form.querySelector<HTMLInputElement>('input[name="auto_init"]')!;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {attachSearchBox} from '../modules/search.ts';
|
||||
import {globMatch} from '../utils/glob.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
@@ -64,9 +65,8 @@ function initRepoSettingsSearchTeamBox() {
|
||||
})));
|
||||
}
|
||||
|
||||
function initRepoSettingsGitHook() {
|
||||
if (!document.querySelector('.page-content.repository.settings.edit.githook')) return;
|
||||
createCodeEditor(document.querySelector<HTMLTextAreaElement>('#content')!);
|
||||
function initRepoSettingsGitHook(form: HTMLFormElement) {
|
||||
createCodeEditor(form.querySelector<HTMLTextAreaElement>('textarea[name=content]')!);
|
||||
}
|
||||
|
||||
function initRepoSettingsBranches() {
|
||||
@@ -137,11 +137,11 @@ function initRepoSettingsOptions() {
|
||||
}
|
||||
|
||||
export function initRepoSettings() {
|
||||
registerGlobalInitFunc('initRepoSettingsGitHook', initRepoSettingsGitHook);
|
||||
if (!document.querySelector('.page-content.repository.settings')) return;
|
||||
initRepoSettingsOptions();
|
||||
initRepoSettingsBranches();
|
||||
initRepoSettingsCollaboration();
|
||||
initRepoSettingsSearchTeamBox();
|
||||
initRepoSettingsGitHook();
|
||||
initRepoSettingsBranchesDrag();
|
||||
}
|
||||
@@ -1,19 +1,15 @@
|
||||
import {hideElem, showElem} from '../utils/dom.ts';
|
||||
import {toggleElem} from '../utils/dom.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
|
||||
export function initUserSettings() {
|
||||
if (!document.querySelector('.user.settings.profile')) return;
|
||||
|
||||
const usernameInput = document.querySelector<HTMLInputElement>('#username');
|
||||
if (!usernameInput) return;
|
||||
usernameInput.addEventListener('input', function () {
|
||||
const prompt = document.querySelector('#name-change-prompt')!;
|
||||
const promptRedirect = document.querySelector('#name-change-redirect-prompt')!;
|
||||
if (this.value.toLowerCase() !== this.getAttribute('data-name')!.toLowerCase()) {
|
||||
showElem(prompt);
|
||||
showElem(promptRedirect);
|
||||
} else {
|
||||
hideElem(prompt);
|
||||
hideElem(promptRedirect);
|
||||
}
|
||||
registerGlobalInitFunc('initUserSettingsForm', (elForm: HTMLFormElement) => {
|
||||
const usernameInput = elForm.querySelector<HTMLInputElement>('input[name=name]')!;
|
||||
usernameInput.addEventListener('input', () => {
|
||||
const prompt = elForm.querySelector('#name-change-prompt')!;
|
||||
const promptRedirect = elForm.querySelector('#name-change-redirect-prompt')!;
|
||||
const showPrompt = usernameInput.value.toLowerCase() !== usernameInput.getAttribute('data-name')!.toLowerCase();
|
||||
toggleElem(prompt, showPrompt);
|
||||
toggleElem(promptRedirect, showPrompt);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -68,6 +68,7 @@ import {initGlobalShortcut} from './modules/shortcut.ts';
|
||||
import {initDevtest} from './modules/devtest.ts';
|
||||
import {initRepoWatch} from './features/repo-watch.ts';
|
||||
import {initPackagesView} from './features/packages.ts';
|
||||
import {initAsyncHashChangeListeners} from './modules/hashchange.ts';
|
||||
|
||||
const initStartTime = performance.now();
|
||||
const initPerformanceTracer = callInitFunctions([
|
||||
@@ -177,3 +178,7 @@ if (initDur > 500) {
|
||||
}
|
||||
|
||||
window.config.frontendInited = true;
|
||||
|
||||
// then init the async hash change handlers, then they can do async content loading by URL hash
|
||||
// e.g.: expand hidden comments or load incomplete diff files
|
||||
initAsyncHashChangeListeners();
|
||||
@@ -0,0 +1,25 @@
|
||||
type HashChangeListener = (e: HashChangeEvent) => void | Promise<void>;
|
||||
|
||||
const hashChangeListeners: Array<{listener: HashChangeListener, priority: number}> = [];
|
||||
let hashChangeListenersInited = false;
|
||||
|
||||
export function registerAsyncHashChangeListener(listener: HashChangeListener, priority = 0): void {
|
||||
hashChangeListeners.push({listener, priority});
|
||||
hashChangeListeners.sort((a, b) => a.priority - b.priority);
|
||||
if (hashChangeListenersInited) listener(new HashChangeEvent('hashchange')); // late registrants missed the initial run
|
||||
}
|
||||
|
||||
export function removeAsyncHashChangeListener(listener: HashChangeListener): void {
|
||||
const index = hashChangeListeners.findIndex((item) => item.listener === listener);
|
||||
if (index !== -1) hashChangeListeners.splice(index, 1);
|
||||
}
|
||||
|
||||
async function runHashChangeListeners(e: HashChangeEvent) {
|
||||
for (const {listener} of hashChangeListeners) await listener(e);
|
||||
}
|
||||
|
||||
export async function initAsyncHashChangeListeners() {
|
||||
hashChangeListenersInited = true;
|
||||
window.addEventListener('hashchange', runHashChangeListeners);
|
||||
await runHashChangeListeners(new HashChangeEvent('hashchange'));
|
||||
}
|
||||
Reference in new issue
Block a user