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:
wxiaoguangandsilverwind authored and GitHub committed 2026-10-18 13:59:51 +00:00
1 parent ae17c14cff
commit f690ea8efd
26 files changed
+122 -125

No files matched your search

+4 -9
View File
@@ -6,7 +6,6 @@ package devtest
import (
"fmt"
"maps"
mathRand "math/rand/v2"
"net/http"
"net/url"
"slices"
@@ -597,7 +596,7 @@ func fillViewRunResponseCurrentJob(ctx *context.Context, resp *actions.ViewRespo
mockLogOptions = append(mockLogOptions, generateMockStepsLogOptions{mockCountFirst: 30, mockCountGeneral: 3, groupRepeat: 20})
resp.State.CurrentJob.Steps = append(resp.State.CurrentJob.Steps, &actions.ViewJobStep{
Summary: "step 2 (mock error)",
Summary: "step 2",
Duration: time.Hour.String(),
Status: actions_model.StatusRunning.String(),
})
@@ -609,20 +608,16 @@ func fillViewRunResponseCurrentJob(ctx *context.Context, resp *actions.ViewRespo
resp.Logs.StepsLog = []*actions.ViewStepLog{}
doSlowResponse := false
doErrorResponse := false
for _, logCur := range req.LogCursors {
if !logCur.Expanded {
continue
}
doSlowResponse = doSlowResponse || logCur.Step == 0
doErrorResponse = doErrorResponse || logCur.Step == 2
resp.Logs.StepsLog = append(resp.Logs.StepsLog, generateMockStepsLog(logCur, mockLogOptions[logCur.Step])...)
}
if doErrorResponse {
if mathRand.Float64() > 0.5 {
ctx.HTTPError(http.StatusInternalServerError, "devtest mock error response")
return
}
if ctx.PathParamInt64("run") == 50 && len(resp.Logs.StepsLog) > 0 {
ctx.HTTPError(http.StatusInternalServerError, "devtest mock error response")
return
}
if doSlowResponse {
time.Sleep(time.Duration(3000) * time.Millisecond)
+1
View File
@@ -6,6 +6,7 @@
<a href="{{AppSubUrl}}/devtest/repo-action-view/runs/30">Run:CanRerunLatest</a>
<a href="{{AppSubUrl}}/devtest/repo-action-view/runs/10/attempts/2">Run:PreviousAttempt</a>
<a href="{{AppSubUrl}}/devtest/repo-action-view/runs/40">Run:ReusableCaller</a>
<a href="{{AppSubUrl}}/devtest/repo-action-view/runs/50/jobs/500">Run:LogsAlwaysFail</a>
</div>
{{template "repo/actions/view_component" (dict
"JobID" (or .JobID 0)
+1 -1
View File
@@ -1,5 +1,5 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content organization new team">
<div role="main" aria-label="{{.Title}}" class="page-content organization new team" data-global-init="initOrgTeamSettings">
{{template "org/header" .}}
<div class="ui container">
<div class="ui grid">
+1 -1
View File
@@ -7,7 +7,7 @@
<div class="ui attached segment">
{{template "base/alert" .}}
{{template "repo/create_helper" .}}
<form class="ui form left-right-form new-repo-form" action="{{.Link}}" method="post">
<form class="ui form left-right-form" action="{{.Link}}" method="post" data-global-init="initRepoNew">
<div id="create-repo-error-message" class="ui negative message tw-text-center tw-hidden"></div>
<div class="inline required field {{if .Err_Owner}}error{{end}}">
<label>{{ctx.Locale.Tr "repo.owner"}}</label>
+2 -2
View File
@@ -35,7 +35,7 @@
{{template "repo/diff/whitespace_dropdown" .}}
{{template "repo/diff/options_dropdown" .}}
{{if .PageIsPullFiles}}
<div id="diff-commit-select" data-merge-base="{{$.CompareInfo.CompareBase}}" data-issuelink="{{$.Issue.Link}}"
<div id="diff-commit-select" data-global-init="initDiffCommitSelect" data-merge-base="{{$.CompareInfo.CompareBase}}" data-issuelink="{{$.Issue.Link}}"
data-queryparams="?style={{if $.IsSplitStyle}}split{{else}}unified{{end}}&whitespace={{$.WhitespaceBehavior}}&show-outdated={{$.ShowOutdatedComments}}"
data-text-filter-changes-by-commit="{{ctx.Locale.Tr "repo.pulls.filter_changes_by_commit"}}">
{{/* the following will be replaced by vue component, but this avoids any loading artifacts till the vue component is initialized */}}
@@ -63,7 +63,7 @@
<div id="diff-container">
{{if $showFileTree}}
{{$.FileIconPoolHTML}}
<div id="diff-file-tree" class="tw-hidden not-mobile" data-locale="{{JsonUtils.EncodeToString (dict
<div id="diff-file-tree" data-global-init="initDiffFileTree" class="tw-hidden not-mobile" data-locale="{{JsonUtils.EncodeToString (dict
"filterFiles" (ctx.Locale.Tr "repo.diff.filter_files")
"filterFilesClear" (ctx.Locale.Tr "repo.diff.filter_files_clear")
"filterByFileExtension" (ctx.Locale.Tr "repo.diff.filter_by_file_extension")
@@ -6,7 +6,7 @@
* TextFilterTitle
*/}}
{{$queryLink := .QueryLink}}
<div class="item ui dropdown custom user-remote-search" data-tooltip-content="{{ctx.Locale.Tr "repo.user_search_tooltip"}}"
<div class="item ui dropdown custom" data-tooltip-content="{{ctx.Locale.Tr "repo.user_search_tooltip"}}" data-global-init="initDropdownUserRemoteSearch"
data-search-url="{{$.UserSearchUrl}}"
data-selected-username="{{$.SelectedUsername}}"
data-action-jump-url="{{QueryBuild $queryLink $.QueryParamKey NIL}}&{{$.QueryParamKey}}={username}"
+1 -1
View File
@@ -1,7 +1,7 @@
<div id="issue-filters" class="issue-list-toolbar">
<div class="issue-list-toolbar-left">
{{if and $.CanWriteIssuesOrPulls .Issues}}
<input type="checkbox" autocomplete="off" class="issue-checkbox-all tw-mr-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}">
<input type="checkbox" autocomplete="off" class="issue-checkbox-all tw-mr-4" title="{{ctx.Locale.Tr "repo.issues.action_check_all"}}" data-global-init="initRepoIssueListCheckboxes">
{{end}}
{{template "repo/issue/openclose" .}}
<!-- Total Tracked Time -->
+1 -1
View File
@@ -7,7 +7,7 @@
{{template "repo/code/recently_pushed_new_branches" dict "RecentBranchesPromptData" .RecentBranchesPromptData}}
{{if .PinnedIssues}}
<div id="issue-pins" {{if .IsRepoAdmin}}data-is-repo-admin{{end}}>
<div id="issue-pins" {{if .IsRepoAdmin}}data-global-init="initIssuePinSort"{{end}}>
{{range .PinnedIssues}}
<div class="issue-card tw-break-anywhere {{if $.IsRepoAdmin}}tw-cursor-grab{{end}}" data-move-url="{{$.Link}}/move_pin" data-issue-id="{{.ID}}">
{{template "repo/issue/card" (dict "Issue" . "Page" $ "isPinnedIssueCard" true)}}
+1 -1
View File
@@ -1,6 +1,6 @@
{{template "repo/settings/layout_head" (dict "pageClass" "repository settings edit githook")}}
<div class="repo-setting-content">
<form class="ui form" action="{{.Link}}" method="post">
<form class="ui form" action="{{.Link}}" method="post" data-global-init="initRepoSettingsGitHook">
<h4 class="ui top attached header flex-left-right">
{{.Hook.Name}}
<div class="tw-font-normal tw-font-sans tw-text-base">
+1 -1
View File
@@ -5,7 +5,7 @@
</h4>
<div class="ui attached segment">
<p>{{ctx.Locale.Tr "settings.profile_desc"}}</p>
<form class="ui form" action="{{.Link}}" method="post">
<form class="ui form" action="{{.Link}}" method="post" data-global-init="initUserSettingsForm">
<div class="required field {{if .Err_Name}}error{{end}}">
<label for="username">{{ctx.Locale.Tr "username"}}
<span class="tw-text-red tw-hidden" id="name-change-prompt"> {{ctx.Locale.Tr "settings.change_username_prompt"}}</span>
+5
View File
@@ -27,3 +27,8 @@ test('job queue refreshes with filter open', async ({page, request}) => {
await expect(page.getByRole('link', {name: 'queued #2'})).toBeVisible();
await expect(waitingFilter).toBeVisible();
});
test('job log line permalink expands its step', async ({page}) => {
await page.goto('/devtest/repo-action-view/runs/10/jobs/1#jobstep-1-23');
await expect(page.getByRole('link', {name: '23', exact: true})).toBeInViewport();
});
+8 -2
View File
@@ -1,7 +1,7 @@
import {test, expect} from '@playwright/test';
import {apiCreateFiles, apiCreatePR, apiCreateRepo, apiCreateUser, apiUserHeaders, loginUser, randomString} from './utils.ts';
test('diff sidebar filtering', async ({page, request}) => {
test('diff sidebar filtering and folded file navigation', async ({page, request}) => {
const user = `df-${randomString(8)}`;
await apiCreateUser(request, user);
const headers = apiUserHeaders(user);
@@ -12,7 +12,7 @@ test('diff sidebar filtering', async ({page, request}) => {
{path: 'src/a.ts', content: 'a\n'},
{path: 'src/b.ts', content: 'b\n'},
{path: 'src/.eslintrc', content: 'e\n'}, // dotfiles count as "No extension"
{path: 'styles/x.css', content: 'x\n'},
{path: 'styles/x.css', content: 'x\n'.repeat(150)},
{path: 'docs/intro.md', content: 'r\n'},
{path: 'Makefile', content: 'm\n'},
], {branch: 'main', newBranch: 'feat', headers});
@@ -34,6 +34,12 @@ test('diff sidebar filtering', async ({page, request}) => {
const treeWidth = (await tree.boundingBox())!.width;
expect(boxesWidth).toBeGreaterThan(treeWidth * 2);
await page.getByLabel('Diff Options').click();
await page.getByRole('menuitem', {name: 'Collapse all files'}).click();
await tree.getByRole('link', {name: 'x.css'}).click();
const cssFileHeader = page.getByRole('link', {name: 'styles/x.css', exact: true});
await expect.poll(async () => (await cssFileHeader.boundingBox())!.y).toBeLessThan(page.viewportSize()!.height / 2);
// search filters tree and file boxes
await search.fill('a.ts');
await expect(items).toHaveText([/a\.ts/]);
+3 -3
View File
@@ -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) {
+3 -3
View File
@@ -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() {
+3 -6
View File
@@ -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);
}
+2 -2
View File
@@ -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);
}
-11
View File
@@ -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);
}
+19 -7
View File
@@ -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);
}
+18 -38
View File
@@ -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);
}
+1 -1
View File
@@ -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();
+1 -5
View File
@@ -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"]')!;
+4 -4
View File
@@ -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();
}
+11 -15
View File
@@ -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);
});
});
}
+5
View File
@@ -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();
+25
View File
@@ -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'));
}