diff options
author | silverwind <me@silverwind.io> | 2025-01-22 08:11:51 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-22 15:11:51 +0800 |
commit | c7f4ca265376700b56b4d0bdd4c879dd9915d1cf (patch) | |
tree | 0e66b84dd4c22290d1d7179ea4c28ed45fdf17e3 /web_src/js | |
parent | 6fe4d1c038dd699269cfbeb1ef435288e2ddf457 (diff) | |
download | gitea-c7f4ca265376700b56b4d0bdd4c879dd9915d1cf.tar.gz gitea-c7f4ca265376700b56b4d0bdd4c879dd9915d1cf.zip |
Enable Typescript `noImplicitAny` (#33322)
Enable `noImplicitAny` and fix all issues.
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Diffstat (limited to 'web_src/js')
61 files changed, 319 insertions, 265 deletions
diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index 40ecbba5e3..876292fc94 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -130,12 +130,12 @@ export default defineComponent({ }, methods: { - changeTab(t) { - this.tab = t; + changeTab(tab: string) { + this.tab = tab; this.updateHistory(); }, - changeReposFilter(filter) { + changeReposFilter(filter: string) { this.reposFilter = filter; this.repos = []; this.page = 1; @@ -218,7 +218,7 @@ export default defineComponent({ this.searchRepos(); }, - changePage(page) { + changePage(page: number) { this.page = page; if (this.page > this.finalPage) { this.page = this.finalPage; @@ -256,7 +256,7 @@ export default defineComponent({ } if (searchedURL === this.searchURL) { - this.repos = json.data.map((webSearchRepo) => { + this.repos = json.data.map((webSearchRepo: any) => { return { ...webSearchRepo.repository, latest_commit_status_state: webSearchRepo.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status @@ -264,7 +264,7 @@ export default defineComponent({ locale_latest_commit_status_state: webSearchRepo.locale_latest_commit_status, }; }); - const count = response.headers.get('X-Total-Count'); + const count = Number(response.headers.get('X-Total-Count')); if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') { this.reposTotalCount = count; } @@ -275,7 +275,7 @@ export default defineComponent({ } }, - repoIcon(repo) { + repoIcon(repo: any) { if (repo.fork) { return 'octicon-repo-forked'; } else if (repo.mirror) { @@ -298,7 +298,7 @@ export default defineComponent({ return commitStatus[status].color; }, - reposFilterKeyControl(e) { + reposFilterKeyControl(e: KeyboardEvent) { switch (e.key) { case 'Enter': document.querySelector<HTMLAnchorElement>('.repo-owner-name-list li.active a')?.click(); diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue index 840acd4b51..16760d1cb1 100644 --- a/web_src/js/components/DiffCommitSelector.vue +++ b/web_src/js/components/DiffCommitSelector.vue @@ -4,6 +4,22 @@ import {SvgIcon} from '../svg.ts'; import {GET} from '../modules/fetch.ts'; import {generateAriaId} from '../modules/fomantic/base.ts'; +type Commit = { + id: string, + hovered: boolean, + selected: boolean, + summary: string, + committer_or_author_name: string, + time: string, + short_sha: string, +} + +type CommitListResult = { + commits: Array<Commit>, + last_review_commit_sha: string, + locale: Record<string, string>, +} + export default defineComponent({ components: {SvgIcon}, data: () => { @@ -16,9 +32,9 @@ export default defineComponent({ locale: { filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'), } as Record<string, string>, - commits: [], + commits: [] as Array<Commit>, hoverActivated: false, - lastReviewCommitSha: null, + lastReviewCommitSha: '', uniqueIdMenu: generateAriaId(), uniqueIdShowAll: generateAriaId(), }; @@ -71,7 +87,7 @@ export default defineComponent({ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { const item = document.activeElement; // try to highlight the selected commits const commitIdx = item?.matches('.item') ? item.getAttribute('data-commit-idx') : null; - if (commitIdx) this.highlight(this.commits[commitIdx]); + if (commitIdx) this.highlight(this.commits[Number(commitIdx)]); } }, onKeyUp(event: KeyboardEvent) { @@ -87,7 +103,7 @@ export default defineComponent({ } } }, - highlight(commit) { + highlight(commit: Commit) { if (!this.hoverActivated) return; const indexSelected = this.commits.findIndex((x) => x.selected); const indexCurrentElem = this.commits.findIndex((x) => x.id === commit.id); @@ -125,10 +141,11 @@ export default defineComponent({ } }); }, + /** Load the commits to show in this dropdown */ async fetchCommits() { const resp = await GET(`${this.issueLink}/commits/list`); - const results = await resp.json(); + const results = await resp.json() as CommitListResult; this.commits.push(...results.commits.map((x) => { x.hovered = false; return x; @@ -166,7 +183,7 @@ export default defineComponent({ * the diff from beginning of PR up to the second clicked commit is * opened */ - commitClickedShift(commit) { + commitClickedShift(commit: Commit) { this.hoverActivated = !this.hoverActivated; commit.selected = true; // Second click -> determine our range and open links accordingly diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue index 792a1aefac..6570c92781 100644 --- a/web_src/js/components/DiffFileList.vue +++ b/web_src/js/components/DiffFileList.vue @@ -18,14 +18,14 @@ function toggleFileList() { } function diffTypeToString(pType: number) { - const diffTypes = { - 1: 'add', - 2: 'modify', - 3: 'del', - 4: 'rename', - 5: 'copy', + const diffTypes: Record<string, string> = { + '1': 'add', + '2': 'modify', + '3': 'del', + '4': 'rename', + '5': 'copy', }; - return diffTypes[pType]; + return diffTypes[String(pType)]; } function diffStatsWidth(adds: number, dels: number) { diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue index 8676c4d37f..d00d03565f 100644 --- a/web_src/js/components/DiffFileTree.vue +++ b/web_src/js/components/DiffFileTree.vue @@ -1,5 +1,5 @@ <script lang="ts" setup> -import DiffFileTreeItem from './DiffFileTreeItem.vue'; +import DiffFileTreeItem, {type Item} from './DiffFileTreeItem.vue'; import {loadMoreFiles} from '../features/repo-diff.ts'; import {toggleElem} from '../utils/dom.ts'; import {diffTreeStore} from '../modules/stores.ts'; @@ -11,7 +11,7 @@ const LOCAL_STORAGE_KEY = 'diff_file_tree_visible'; const store = diffTreeStore(); const fileTree = computed(() => { - const result = []; + const result: Array<Item> = []; for (const file of store.files) { // Split file into directories const splits = file.Name.split('/'); @@ -24,15 +24,10 @@ const fileTree = computed(() => { if (index === splits.length) { isFile = true; } - let newParent = { + let newParent: Item = { name: split, children: [], isFile, - } as { - name: string, - children: any[], - isFile: boolean, - file?: any, }; if (isFile === true) { diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue index 9a21a8ac10..d3be10e3e9 100644 --- a/web_src/js/components/DiffFileTreeItem.vue +++ b/web_src/js/components/DiffFileTreeItem.vue @@ -1,5 +1,5 @@ <script lang="ts" setup> -import {SvgIcon} from '../svg.ts'; +import {SvgIcon, type SvgName} from '../svg.ts'; import {diffTreeStore} from '../modules/stores.ts'; import {ref} from 'vue'; @@ -11,7 +11,7 @@ type File = { IsSubmodule: boolean; } -type Item = { +export type Item = { name: string; isFile: boolean; file?: File; @@ -26,14 +26,14 @@ const store = diffTreeStore(); const collapsed = ref(false); function getIconForDiffType(pType: number) { - const diffTypes = { - 1: {name: 'octicon-diff-added', classes: ['text', 'green']}, - 2: {name: 'octicon-diff-modified', classes: ['text', 'yellow']}, - 3: {name: 'octicon-diff-removed', classes: ['text', 'red']}, - 4: {name: 'octicon-diff-renamed', classes: ['text', 'teal']}, - 5: {name: 'octicon-diff-renamed', classes: ['text', 'green']}, // there is no octicon for copied, so renamed should be ok + const diffTypes: Record<string, {name: SvgName, classes: Array<string>}> = { + '1': {name: 'octicon-diff-added', classes: ['text', 'green']}, + '2': {name: 'octicon-diff-modified', classes: ['text', 'yellow']}, + '3': {name: 'octicon-diff-removed', classes: ['text', 'red']}, + '4': {name: 'octicon-diff-renamed', classes: ['text', 'teal']}, + '5': {name: 'octicon-diff-renamed', classes: ['text', 'green']}, // there is no octicon for copied, so renamed should be ok }; - return diffTypes[pType]; + return diffTypes[String(pType)]; } function fileIcon(file: File) { diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index 1393a7f258..4f291f5ca1 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -36,17 +36,17 @@ const forceMerge = computed(() => { }); watch(mergeStyle, (val) => { - mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e) => e.name === val); + mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e: any) => e.name === val); for (const elem of document.querySelectorAll('[data-pull-merge-style]')) { toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val); } }); onMounted(() => { - mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0); + mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0); - let mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name; - if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed)?.name; + let mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name; + if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed)?.name; switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow); document.addEventListener('mouseup', hideMergeStyleMenu); diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 79b43a3746..03c8464060 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -6,6 +6,7 @@ import {createElementFromAttrs, toggleElem} from '../utils/dom.ts'; import {formatDatetime} from '../utils/time.ts'; import {renderAnsi} from '../render/ansi.ts'; import {POST, DELETE} from '../modules/fetch.ts'; +import type {IntervalId} from '../types.ts'; // see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts" type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked'; @@ -24,6 +25,20 @@ type LogLineCommand = { prefix: string, } +type Job = { + id: number; + name: string; + status: RunStatus; + canRerun: boolean; + duration: string; +} + +type Step = { + summary: string, + duration: string, + status: RunStatus, +} + function parseLineCommand(line: LogLine): LogLineCommand | null { for (const prefix of LogLinePrefixesGroup) { if (line.message.startsWith(prefix)) { @@ -77,7 +92,7 @@ export default defineComponent({ default: '', }, locale: { - type: Object as PropType<Record<string, string>>, + type: Object as PropType<Record<string, any>>, default: null, }, }, @@ -86,10 +101,10 @@ export default defineComponent({ const {autoScroll, expandRunning} = getLocaleStorageOptions(); return { // internal state - loadingAbortController: null, - intervalID: null, - currentJobStepsStates: [], - artifacts: [], + loadingAbortController: null as AbortController | null, + intervalID: null as IntervalId | null, + currentJobStepsStates: [] as Array<Record<string, any>>, + artifacts: [] as Array<Record<string, any>>, onHoverRerunIndex: -1, menuVisible: false, isFullScreen: false, @@ -122,7 +137,7 @@ export default defineComponent({ // canRerun: false, // duration: '', // }, - ], + ] as Array<Job>, commit: { localeCommit: '', localePushedBy: '', @@ -148,7 +163,7 @@ export default defineComponent({ // duration: '', // status: '', // } - ], + ] as Array<Step>, }, }; }, @@ -194,7 +209,7 @@ export default defineComponent({ // get the job step logs container ('.job-step-logs') getJobStepLogsContainer(stepIndex: number): HTMLElement { - return this.$refs.logs[stepIndex]; + return (this.$refs.logs as any)[stepIndex]; }, // get the active logs container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group` @@ -205,7 +220,7 @@ export default defineComponent({ }, // begin a log group beginLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) { - const el = this.$refs.logs[stepIndex]; + const el = (this.$refs.logs as any)[stepIndex]; const elJobLogGroupSummary = createElementFromAttrs('summary', {class: 'job-log-group-summary'}, this.createLogLine(stepIndex, startTime, { index: line.index, @@ -223,7 +238,7 @@ export default defineComponent({ }, // end a log group endLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) { - const el = this.$refs.logs[stepIndex]; + const el = (this.$refs.logs as any)[stepIndex]; el._stepLogsActiveContainer = null; el.append(this.createLogLine(stepIndex, startTime, { index: line.index, @@ -393,7 +408,7 @@ export default defineComponent({ if (this.menuVisible) this.menuVisible = false; }, - toggleTimeDisplay(type: string) { + toggleTimeDisplay(type: 'seconds' | 'stamp') { this.timeVisible[`log-time-${type}`] = !this.timeVisible[`log-time-${type}`]; for (const el of (this.$refs.steps as HTMLElement).querySelectorAll(`.log-time-${type}`)) { toggleElem(el, this.timeVisible[`log-time-${type}`]); @@ -422,9 +437,10 @@ export default defineComponent({ const selectedLogStep = window.location.hash; if (!selectedLogStep) return; const [_, step, _line] = selectedLogStep.split('-'); - if (!this.currentJobStepsStates[step]) return; - if (!this.currentJobStepsStates[step].expanded && this.currentJobStepsStates[step].cursor === null) { - this.currentJobStepsStates[step].expanded = true; + const stepNum = Number(step); + if (!this.currentJobStepsStates[stepNum]) return; + if (!this.currentJobStepsStates[stepNum].expanded && this.currentJobStepsStates[stepNum].cursor === null) { + this.currentJobStepsStates[stepNum].expanded = true; // need to await for load job if the step log is loaded for the first time // so logline can be selected by querySelector await this.loadJob(); diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue index 1295e15582..77b85bd7e2 100644 --- a/web_src/js/components/RepoActivityTopAuthors.vue +++ b/web_src/js/components/RepoActivityTopAuthors.vue @@ -1,4 +1,5 @@ <script lang="ts" setup> +// @ts-expect-error - module exports no types import {VueBarGraph} from 'vue-bar-graph'; import {computed, onMounted, ref} from 'vue'; diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue index 7e35d55b2f..fa5c75af99 100644 --- a/web_src/js/components/RepoBranchTagSelector.vue +++ b/web_src/js/components/RepoBranchTagSelector.vue @@ -157,7 +157,7 @@ export default defineComponent({ // @ts-expect-error - el is unknown type return (el && el.length) ? el[0] : null; }, - keydown(e) { + keydown(e: KeyboardEvent) { if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { e.preventDefault(); @@ -181,7 +181,7 @@ export default defineComponent({ this.menuVisible = false; } }, - handleTabSwitch(selectedTab) { + handleTabSwitch(selectedTab: SelectedTab) { this.selectedTab = selectedTab; this.focusSearchField(); this.loadTabItems(); diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue index 5d2c74b6a9..6ad2c848b1 100644 --- a/web_src/js/components/RepoContributors.vue +++ b/web_src/js/components/RepoContributors.vue @@ -80,10 +80,10 @@ export default defineComponent({ sortedContributors: {} as Record<string, any>, type: 'commits', contributorsStats: {} as Record<string, any>, - xAxisStart: null, - xAxisEnd: null, - xAxisMin: null, - xAxisMax: null, + xAxisStart: null as number | null, + xAxisEnd: null as number | null, + xAxisMin: null as number | null, + xAxisMax: null as number | null, }), mounted() { this.fetchGraphData(); @@ -99,7 +99,7 @@ export default defineComponent({ }, methods: { sortContributors() { - const contributors = this.filterContributorWeeksByDateRange(); + const contributors: Record<string, any> = this.filterContributorWeeksByDateRange(); const criteria = `total_${this.type}`; this.sortedContributors = Object.values(contributors) .filter((contributor) => contributor[criteria] !== 0) @@ -158,7 +158,7 @@ export default defineComponent({ }, filterContributorWeeksByDateRange() { - const filteredData = {}; + const filteredData: Record<string, any> = {}; const data = this.contributorsStats; for (const key of Object.keys(data)) { const user = data[key]; @@ -196,7 +196,7 @@ export default defineComponent({ // Normally, chartjs handles this automatically, but it will resize the graph when you // zoom, pan etc. I think resizing the graph makes it harder to compare things visually. const maxValue = Math.max( - ...this.totalStats.weeks.map((o) => o[this.type]), + ...this.totalStats.weeks.map((o: Record<string, any>) => o[this.type]), ); const [coefficient, exp] = maxValue.toExponential().split('e').map(Number); if (coefficient % 1 === 0) return maxValue; @@ -208,7 +208,7 @@ export default defineComponent({ // for contributors' graph. If I let chartjs do this for me, it will choose different // maxY value for each contributors' graph which again makes it harder to compare. const maxValue = Math.max( - ...this.sortedContributors.map((c) => c.max_contribution_type), + ...this.sortedContributors.map((c: Record<string, any>) => c.max_contribution_type), ); const [coefficient, exp] = maxValue.toExponential().split('e').map(Number); if (coefficient % 1 === 0) return maxValue; @@ -232,8 +232,8 @@ export default defineComponent({ }, updateOtherCharts({chart}: {chart: Chart}, reset: boolean = false) { - const minVal = chart.options.scales.x.min; - const maxVal = chart.options.scales.x.max; + const minVal = Number(chart.options.scales.x.min); + const maxVal = Number(chart.options.scales.x.max); if (reset) { this.xAxisMin = this.xAxisStart; this.xAxisMax = this.xAxisEnd; diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue index 63214d0bf5..9eaf824035 100644 --- a/web_src/js/components/ScopedAccessTokenSelector.vue +++ b/web_src/js/components/ScopedAccessTokenSelector.vue @@ -35,7 +35,7 @@ onUnmounted(() => { document.querySelector('#scoped-access-submit').removeEventListener('click', onClickSubmit); }); -function onClickSubmit(e) { +function onClickSubmit(e: Event) { e.preventDefault(); const warningEl = document.querySelector('#scoped-access-warning'); diff --git a/web_src/js/features/admin/common.ts b/web_src/js/features/admin/common.ts index 6c725a3efe..b991749d81 100644 --- a/web_src/js/features/admin/common.ts +++ b/web_src/js/features/admin/common.ts @@ -90,7 +90,7 @@ export function initAdminCommon(): void { onOAuth2UseCustomURLChange(applyDefaultValues); } - function onOAuth2UseCustomURLChange(applyDefaultValues) { + function onOAuth2UseCustomURLChange(applyDefaultValues: boolean) { const provider = document.querySelector<HTMLInputElement>('#oauth2_provider').value; hideElem('.oauth2_use_custom_url_field'); for (const input of document.querySelectorAll<HTMLInputElement>('.oauth2_use_custom_url_field input[required]')) { diff --git a/web_src/js/features/citation.ts b/web_src/js/features/citation.ts index fc5bb38f0a..3c9fe0afc8 100644 --- a/web_src/js/features/citation.ts +++ b/web_src/js/features/citation.ts @@ -5,9 +5,13 @@ const {pageData} = window.config; async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) { const [{Cite, plugins}] = await Promise.all([ + // @ts-expect-error: module exports no types import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'), + // @ts-expect-error: module exports no types import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'), + // @ts-expect-error: module exports no types import(/* webpackChunkName: "citation-js-bibtex" */'@citation-js/plugin-bibtex'), + // @ts-expect-error: module exports no types import(/* webpackChunkName: "citation-js-csl" */'@citation-js/plugin-csl'), ]); const {citationFileContent} = pageData; diff --git a/web_src/js/features/common-button.ts b/web_src/js/features/common-button.ts index 3162557b9b..7aebdd8dd5 100644 --- a/web_src/js/features/common-button.ts +++ b/web_src/js/features/common-button.ts @@ -74,10 +74,10 @@ export function initGlobalDeleteButton(): void { } } -function onShowPanelClick(e) { +function onShowPanelClick(e: MouseEvent) { // a '.show-panel' element can show a panel, by `data-panel="selector"` // if it has "toggle" class, it toggles the panel - const el = e.currentTarget; + const el = e.currentTarget as HTMLElement; e.preventDefault(); const sel = el.getAttribute('data-panel'); if (el.classList.contains('toggle')) { @@ -87,9 +87,9 @@ function onShowPanelClick(e) { } } -function onHidePanelClick(e) { +function onHidePanelClick(e: MouseEvent) { // a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"` - const el = e.currentTarget; + const el = e.currentTarget as HTMLElement; e.preventDefault(); let sel = el.getAttribute('data-panel'); if (sel) { @@ -98,13 +98,13 @@ function onHidePanelClick(e) { } sel = el.getAttribute('data-panel-closest'); if (sel) { - hideElem(el.parentNode.closest(sel)); + hideElem((el.parentNode as HTMLElement).closest(sel)); return; } throw new Error('no panel to hide'); // should never happen, otherwise there is a bug in code } -function onShowModalClick(e) { +function onShowModalClick(e: MouseEvent) { // A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute. // Each "data-modal-{target}" attribute will be filled to target element's value or text-content. // * First, try to query '#target' @@ -112,7 +112,7 @@ function onShowModalClick(e) { // * Then, try to query '.target' // * Then, try to query 'target' as HTML tag // If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set. - const el = e.currentTarget; + const el = e.currentTarget as HTMLElement; e.preventDefault(); const modalSelector = el.getAttribute('data-modal'); const elModal = document.querySelector(modalSelector); @@ -137,9 +137,9 @@ function onShowModalClick(e) { } if (attrTargetAttr) { - attrTarget[camelize(attrTargetAttr)] = attrib.value; + (attrTarget as any)[camelize(attrTargetAttr)] = attrib.value; } else if (attrTarget.matches('input, textarea')) { - attrTarget.value = attrib.value; // FIXME: add more supports like checkbox + (attrTarget as HTMLInputElement | HTMLTextAreaElement).value = attrib.value; // FIXME: add more supports like checkbox } else { attrTarget.textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p } diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts index bc72f4089a..2da481e521 100644 --- a/web_src/js/features/common-fetch-action.ts +++ b/web_src/js/features/common-fetch-action.ts @@ -75,7 +75,10 @@ async function formFetchAction(formEl: HTMLFormElement, e: SubmitEvent) { } let reqUrl = formActionUrl; - const reqOpt = {method: formMethod.toUpperCase(), body: null}; + const reqOpt = { + method: formMethod.toUpperCase(), + body: null as FormData | null, + }; if (formMethod.toLowerCase() === 'get') { const params = new URLSearchParams(); for (const [key, value] of formData) { diff --git a/web_src/js/features/common-form.ts b/web_src/js/features/common-form.ts index 8532d397cd..7321d80c44 100644 --- a/web_src/js/features/common-form.ts +++ b/web_src/js/features/common-form.ts @@ -17,13 +17,13 @@ export function initGlobalEnterQuickSubmit() { if (e.key !== 'Enter') return; const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey); if (hasCtrlOrMeta && e.target.matches('textarea')) { - if (handleGlobalEnterQuickSubmit(e.target)) { + if (handleGlobalEnterQuickSubmit(e.target as HTMLElement)) { e.preventDefault(); } } else if (e.target.matches('input') && !e.target.closest('form')) { // input in a normal form could handle Enter key by default, so we only handle the input outside a form // eslint-disable-next-line unicorn/no-lonely-if - if (handleGlobalEnterQuickSubmit(e.target)) { + if (handleGlobalEnterQuickSubmit(e.target as HTMLElement)) { e.preventDefault(); } } diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index bba50a1296..d3773a89c4 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -29,10 +29,10 @@ let elementIdCounter = 0; /** * validate if the given textarea is non-empty. - * @param {HTMLElement} textarea - The textarea element to be validated. + * @param {HTMLTextAreaElement} textarea - The textarea element to be validated. * @returns {boolean} returns true if validation succeeded. */ -export function validateTextareaNonEmpty(textarea) { +export function validateTextareaNonEmpty(textarea: HTMLTextAreaElement) { // When using EasyMDE, the original edit area HTML element is hidden, breaking HTML5 input validation. // The workaround (https://github.com/sparksuite/simplemde-markdown-editor/issues/324) doesn't work with contenteditable, so we just show an alert. if (!textarea.value) { @@ -49,16 +49,25 @@ export function validateTextareaNonEmpty(textarea) { return true; } +type Heights = { + minHeight?: string, + height?: string, + maxHeight?: string, +}; + type ComboMarkdownEditorOptions = { - editorHeights?: {minHeight?: string, height?: string, maxHeight?: string}, + editorHeights?: Heights, easyMDEOptions?: EasyMDE.Options, }; +type ComboMarkdownEditorTextarea = HTMLTextAreaElement & {_giteaComboMarkdownEditor: any}; +type ComboMarkdownEditorContainer = HTMLElement & {_giteaComboMarkdownEditor?: any}; + export class ComboMarkdownEditor { static EventEditorContentChanged = EventEditorContentChanged; static EventUploadStateChanged = EventUploadStateChanged; - public container : HTMLElement; + public container: HTMLElement; options: ComboMarkdownEditorOptions; @@ -70,7 +79,7 @@ export class ComboMarkdownEditor { easyMDEToolbarActions: any; easyMDEToolbarDefault: any; - textarea: HTMLTextAreaElement & {_giteaComboMarkdownEditor: any}; + textarea: ComboMarkdownEditorTextarea; textareaMarkdownToolbar: HTMLElement; textareaAutosize: any; @@ -81,7 +90,7 @@ export class ComboMarkdownEditor { previewUrl: string; previewContext: string; - constructor(container, options:ComboMarkdownEditorOptions = {}) { + constructor(container: ComboMarkdownEditorContainer, options:ComboMarkdownEditorOptions = {}) { if (container._giteaComboMarkdownEditor) throw new Error('ComboMarkdownEditor already initialized'); container._giteaComboMarkdownEditor = this; this.options = options; @@ -98,7 +107,7 @@ export class ComboMarkdownEditor { await this.switchToUserPreference(); } - applyEditorHeights(el, heights) { + applyEditorHeights(el: HTMLElement, heights: Heights) { if (!heights) return; if (heights.minHeight) el.style.minHeight = heights.minHeight; if (heights.height) el.style.height = heights.height; @@ -283,7 +292,7 @@ export class ComboMarkdownEditor { ]; } - parseEasyMDEToolbar(easyMde: typeof EasyMDE, actions) { + parseEasyMDEToolbar(easyMde: typeof EasyMDE, actions: any) { this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(easyMde, this); const processed = []; for (const action of actions) { @@ -332,21 +341,21 @@ export class ComboMarkdownEditor { this.easyMDE = new EasyMDE(easyMDEOpt); this.easyMDE.codemirror.on('change', () => triggerEditorContentChanged(this.container)); this.easyMDE.codemirror.setOption('extraKeys', { - 'Cmd-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()), - 'Ctrl-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()), - Enter: (cm) => { + 'Cmd-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()), + 'Ctrl-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()), + Enter: (cm: any) => { const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); if (!tributeContainer || tributeContainer.style.display === 'none') { cm.execCommand('newlineAndIndent'); } }, - Up: (cm) => { + Up: (cm: any) => { const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); if (!tributeContainer || tributeContainer.style.display === 'none') { return cm.execCommand('goLineUp'); } }, - Down: (cm) => { + Down: (cm: any) => { const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); if (!tributeContainer || tributeContainer.style.display === 'none') { return cm.execCommand('goLineDown'); @@ -354,14 +363,14 @@ export class ComboMarkdownEditor { }, }); this.applyEditorHeights(this.container.querySelector('.CodeMirror-scroll'), this.options.editorHeights); - await attachTribute(this.easyMDE.codemirror.getInputField(), {mentions: true, emoji: true}); + await attachTribute(this.easyMDE.codemirror.getInputField()); if (this.dropzone) { initEasyMDEPaste(this.easyMDE, this.dropzone); } hideElem(this.textareaMarkdownToolbar); } - value(v = undefined) { + value(v: any = undefined) { if (v === undefined) { if (this.easyMDE) { return this.easyMDE.value(); @@ -402,7 +411,7 @@ export class ComboMarkdownEditor { } } -export function getComboMarkdownEditor(el) { +export function getComboMarkdownEditor(el: any) { if (!el) return null; if (el.length) el = el[0]; return el._giteaComboMarkdownEditor; diff --git a/web_src/js/features/comp/EditorMarkdown.ts b/web_src/js/features/comp/EditorMarkdown.ts index 08306531f1..6e66c15763 100644 --- a/web_src/js/features/comp/EditorMarkdown.ts +++ b/web_src/js/features/comp/EditorMarkdown.ts @@ -1,10 +1,10 @@ export const EventEditorContentChanged = 'ce-editor-content-changed'; -export function triggerEditorContentChanged(target) { +export function triggerEditorContentChanged(target: HTMLElement) { target.dispatchEvent(new CustomEvent(EventEditorContentChanged, {bubbles: true})); } -export function textareaInsertText(textarea, value) { +export function textareaInsertText(textarea: HTMLTextAreaElement, value: string) { const startPos = textarea.selectionStart; const endPos = textarea.selectionEnd; textarea.value = textarea.value.substring(0, startPos) + value + textarea.value.substring(endPos); @@ -20,7 +20,7 @@ type TextareaValueSelection = { selEnd: number; } -function handleIndentSelection(textarea: HTMLTextAreaElement, e) { +function handleIndentSelection(textarea: HTMLTextAreaElement, e: KeyboardEvent) { const selStart = textarea.selectionStart; const selEnd = textarea.selectionEnd; if (selEnd === selStart) return; // do not process when no selection @@ -188,7 +188,7 @@ function isTextExpanderShown(textarea: HTMLElement): boolean { return Boolean(textarea.closest('text-expander')?.querySelector('.suggestions')); } -export function initTextareaMarkdown(textarea) { +export function initTextareaMarkdown(textarea: HTMLTextAreaElement) { textarea.addEventListener('keydown', (e) => { if (isTextExpanderShown(textarea)) return; if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) { diff --git a/web_src/js/features/comp/EditorUpload.ts b/web_src/js/features/comp/EditorUpload.ts index 89982747ea..f6d5731422 100644 --- a/web_src/js/features/comp/EditorUpload.ts +++ b/web_src/js/features/comp/EditorUpload.ts @@ -8,43 +8,46 @@ import { generateMarkdownLinkForAttachment, } from '../dropzone.ts'; import type CodeMirror from 'codemirror'; +import type EasyMDE from 'easymde'; +import type {DropzoneFile} from 'dropzone'; let uploadIdCounter = 0; export const EventUploadStateChanged = 'ce-upload-state-changed'; -export function triggerUploadStateChanged(target) { +export function triggerUploadStateChanged(target: HTMLElement) { target.dispatchEvent(new CustomEvent(EventUploadStateChanged, {bubbles: true})); } -function uploadFile(dropzoneEl, file) { +function uploadFile(dropzoneEl: HTMLElement, file: File) { return new Promise((resolve) => { const curUploadId = uploadIdCounter++; - file._giteaUploadId = curUploadId; + (file as any)._giteaUploadId = curUploadId; const dropzoneInst = dropzoneEl.dropzone; - const onUploadDone = ({file}) => { + const onUploadDone = ({file}: {file: any}) => { if (file._giteaUploadId === curUploadId) { dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone); resolve(file); } }; dropzoneInst.on(DropzoneCustomEventUploadDone, onUploadDone); - dropzoneInst.handleFiles([file]); + // FIXME: this is not entirely correct because `file` does not satisfy DropzoneFile (we have abused the Dropzone for long time) + dropzoneInst.addFile(file as DropzoneFile); }); } class TextareaEditor { - editor : HTMLTextAreaElement; + editor: HTMLTextAreaElement; - constructor(editor) { + constructor(editor: HTMLTextAreaElement) { this.editor = editor; } - insertPlaceholder(value) { + insertPlaceholder(value: string) { textareaInsertText(this.editor, value); } - replacePlaceholder(oldVal, newVal) { + replacePlaceholder(oldVal: string, newVal: string) { const editor = this.editor; const startPos = editor.selectionStart; const endPos = editor.selectionEnd; @@ -65,11 +68,11 @@ class TextareaEditor { class CodeMirrorEditor { editor: CodeMirror.EditorFromTextArea; - constructor(editor) { + constructor(editor: CodeMirror.EditorFromTextArea) { this.editor = editor; } - insertPlaceholder(value) { + insertPlaceholder(value: string) { const editor = this.editor; const startPoint = editor.getCursor('start'); const endPoint = editor.getCursor('end'); @@ -80,7 +83,7 @@ class CodeMirrorEditor { triggerEditorContentChanged(editor.getTextArea()); } - replacePlaceholder(oldVal, newVal) { + replacePlaceholder(oldVal: string, newVal: string) { const editor = this.editor; const endPoint = editor.getCursor('end'); if (editor.getSelection() === oldVal) { @@ -96,7 +99,7 @@ class CodeMirrorEditor { } } -async function handleUploadFiles(editor, dropzoneEl, files, e) { +async function handleUploadFiles(editor: CodeMirrorEditor | TextareaEditor, dropzoneEl: HTMLElement, files: Array<File> | FileList, e: Event) { e.preventDefault(); for (const file of files) { const name = file.name.slice(0, file.name.lastIndexOf('.')); @@ -109,13 +112,13 @@ async function handleUploadFiles(editor, dropzoneEl, files, e) { } } -export function removeAttachmentLinksFromMarkdown(text, fileUuid) { +export function removeAttachmentLinksFromMarkdown(text: string, fileUuid: string) { text = text.replace(new RegExp(`!?\\[([^\\]]+)\\]\\(/?attachments/${fileUuid}\\)`, 'g'), ''); text = text.replace(new RegExp(`<img[^>]+src="/?attachments/${fileUuid}"[^>]*>`, 'g'), ''); return text; } -function handleClipboardText(textarea, e, {text, isShiftDown}) { +function handleClipboardText(textarea: HTMLTextAreaElement, e: ClipboardEvent, text: string, isShiftDown: boolean) { // pasting with "shift" means "paste as original content" in most applications if (isShiftDown) return; // let the browser handle it @@ -131,7 +134,7 @@ function handleClipboardText(textarea, e, {text, isShiftDown}) { } // extract text and images from "paste" event -function getPastedContent(e) { +function getPastedContent(e: ClipboardEvent) { const images = []; for (const item of e.clipboardData?.items ?? []) { if (item.type?.startsWith('image/')) { @@ -142,8 +145,8 @@ function getPastedContent(e) { return {text, images}; } -export function initEasyMDEPaste(easyMDE, dropzoneEl) { - const editor = new CodeMirrorEditor(easyMDE.codemirror); +export function initEasyMDEPaste(easyMDE: EasyMDE, dropzoneEl: HTMLElement) { + const editor = new CodeMirrorEditor(easyMDE.codemirror as any); easyMDE.codemirror.on('paste', (_, e) => { const {images} = getPastedContent(e); if (!images.length) return; @@ -160,28 +163,28 @@ export function initEasyMDEPaste(easyMDE, dropzoneEl) { }); } -export function initTextareaEvents(textarea, dropzoneEl) { +export function initTextareaEvents(textarea: HTMLTextAreaElement, dropzoneEl: HTMLElement) { let isShiftDown = false; - textarea.addEventListener('keydown', (e) => { + textarea.addEventListener('keydown', (e: KeyboardEvent) => { if (e.shiftKey) isShiftDown = true; }); - textarea.addEventListener('keyup', (e) => { + textarea.addEventListener('keyup', (e: KeyboardEvent) => { if (!e.shiftKey) isShiftDown = false; }); - textarea.addEventListener('paste', (e) => { + textarea.addEventListener('paste', (e: ClipboardEvent) => { const {images, text} = getPastedContent(e); if (images.length && dropzoneEl) { handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, images, e); } else if (text) { - handleClipboardText(textarea, e, {text, isShiftDown}); + handleClipboardText(textarea, e, text, isShiftDown); } }); - textarea.addEventListener('drop', (e) => { + textarea.addEventListener('drop', (e: DragEvent) => { if (!e.dataTransfer.files.length) return; if (!dropzoneEl) return; handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, e.dataTransfer.files, e); }); - dropzoneEl?.dropzone.on(DropzoneCustomEventRemovedFile, ({fileUuid}) => { + dropzoneEl?.dropzone.on(DropzoneCustomEventRemovedFile, ({fileUuid}: {fileUuid: string}) => { const newText = removeAttachmentLinksFromMarkdown(textarea.value, fileUuid); if (textarea.value !== newText) textarea.value = newText; }); diff --git a/web_src/js/features/comp/QuickSubmit.ts b/web_src/js/features/comp/QuickSubmit.ts index 385acb319f..0a41f69132 100644 --- a/web_src/js/features/comp/QuickSubmit.ts +++ b/web_src/js/features/comp/QuickSubmit.ts @@ -1,6 +1,6 @@ import {querySingleVisibleElem} from '../../utils/dom.ts'; -export function handleGlobalEnterQuickSubmit(target) { +export function handleGlobalEnterQuickSubmit(target: HTMLElement) { let form = target.closest('form'); if (form) { if (!form.checkValidity()) { diff --git a/web_src/js/features/comp/SearchUserBox.ts b/web_src/js/features/comp/SearchUserBox.ts index 2e3b3f83be..9fedb3ed24 100644 --- a/web_src/js/features/comp/SearchUserBox.ts +++ b/web_src/js/features/comp/SearchUserBox.ts @@ -14,7 +14,7 @@ export function initCompSearchUserBox() { minCharacters: 2, apiSettings: { url: `${appSubUrl}/user/search_candidates?q={query}`, - onResponse(response) { + onResponse(response: any) { const resultItems = []; const searchQuery = searchUserBox.querySelector('input').value; const searchQueryUppercase = searchQuery.toUpperCase(); diff --git a/web_src/js/features/comp/TextExpander.ts b/web_src/js/features/comp/TextExpander.ts index bad8d2e59d..dc08d1739d 100644 --- a/web_src/js/features/comp/TextExpander.ts +++ b/web_src/js/features/comp/TextExpander.ts @@ -5,6 +5,7 @@ import {parseIssueHref, parseRepoOwnerPathInfo} from '../../utils.ts'; import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts'; import {getIssueColor, getIssueIcon} from '../issue.ts'; import {debounce} from 'perfect-debounce'; +import type TextExpanderElement from '@github/text-expander-element'; const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => { const issuePathInfo = parseIssueHref(window.location.href); @@ -32,8 +33,8 @@ const debouncedSuggestIssues = debounce((key: string, text: string) => new Promi resolve({matched: true, fragment: ul}); }), 100); -export function initTextExpander(expander) { - expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => { +export function initTextExpander(expander: TextExpanderElement) { + expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}: Record<string, any>) => { if (key === ':') { const matches = matchEmoji(text); if (!matches.length) return provide({matched: false}); @@ -84,7 +85,7 @@ export function initTextExpander(expander) { provide(debouncedSuggestIssues(key, text)); } }); - expander?.addEventListener('text-expander-value', ({detail}) => { + expander?.addEventListener('text-expander-value', ({detail}: Record<string, any>) => { if (detail?.item) { // add a space after @mentions and #issue as it's likely the user wants one const suffix = ['@', '#'].includes(detail.key) ? ' ' : ''; diff --git a/web_src/js/features/contextpopup.ts b/web_src/js/features/contextpopup.ts index 33eead8431..7477331dbe 100644 --- a/web_src/js/features/contextpopup.ts +++ b/web_src/js/features/contextpopup.ts @@ -4,11 +4,11 @@ import {parseIssueHref} from '../utils.ts'; import {createTippy} from '../modules/tippy.ts'; export function initContextPopups() { - const refIssues = document.querySelectorAll('.ref-issue'); + const refIssues = document.querySelectorAll<HTMLElement>('.ref-issue'); attachRefIssueContextPopup(refIssues); } -export function attachRefIssueContextPopup(refIssues) { +export function attachRefIssueContextPopup(refIssues: NodeListOf<HTMLElement>) { for (const refIssue of refIssues) { if (refIssue.classList.contains('ref-external-issue')) continue; diff --git a/web_src/js/features/copycontent.ts b/web_src/js/features/copycontent.ts index af867463b2..4bc9281a35 100644 --- a/web_src/js/features/copycontent.ts +++ b/web_src/js/features/copycontent.ts @@ -46,7 +46,7 @@ export function initCopyContent() { showTemporaryTooltip(btn, i18n.copy_success); } else { if (isRasterImage) { - const success = await clippie(await convertImage(content, 'image/png')); + const success = await clippie(await convertImage(content as Blob, 'image/png')); showTemporaryTooltip(btn, success ? i18n.copy_success : i18n.copy_error); } else { showTemporaryTooltip(btn, i18n.copy_error); diff --git a/web_src/js/features/dropzone.ts b/web_src/js/features/dropzone.ts index 666c645230..b2ba7651c4 100644 --- a/web_src/js/features/dropzone.ts +++ b/web_src/js/features/dropzone.ts @@ -6,16 +6,18 @@ import {GET, POST} from '../modules/fetch.ts'; import {showErrorToast} from '../modules/toast.ts'; import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts'; import {isImageFile, isVideoFile} from '../utils.ts'; -import type {DropzoneFile} from 'dropzone/index.js'; +import type {DropzoneFile, DropzoneOptions} from 'dropzone/index.js'; const {csrfToken, i18n} = window.config; +type CustomDropzoneFile = DropzoneFile & {uuid: string}; + // dropzone has its owner event dispatcher (emitter) export const DropzoneCustomEventReloadFiles = 'dropzone-custom-reload-files'; export const DropzoneCustomEventRemovedFile = 'dropzone-custom-removed-file'; export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done'; -async function createDropzone(el, opts) { +async function createDropzone(el: HTMLElement, opts: DropzoneOptions) { const [{default: Dropzone}] = await Promise.all([ import(/* webpackChunkName: "dropzone" */'dropzone'), import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'), @@ -23,7 +25,7 @@ async function createDropzone(el, opts) { return new Dropzone(el, opts); } -export function generateMarkdownLinkForAttachment(file, {width, dppx}: {width?: number, dppx?: number} = {}) { +export function generateMarkdownLinkForAttachment(file: Partial<CustomDropzoneFile>, {width, dppx}: {width?: number, dppx?: number} = {}) { let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`; if (isImageFile(file)) { fileMarkdown = `!${fileMarkdown}`; @@ -43,7 +45,7 @@ export function generateMarkdownLinkForAttachment(file, {width, dppx}: {width?: return fileMarkdown; } -function addCopyLink(file) { +function addCopyLink(file: Partial<CustomDropzoneFile>) { // Create a "Copy Link" element, to conveniently copy the image or file link as Markdown to the clipboard // The "<a>" element has a hardcoded cursor: pointer because the default is overridden by .dropzone const copyLinkEl = createElementFromHTML(` @@ -58,6 +60,8 @@ function addCopyLink(file) { file.previewTemplate.append(copyLinkEl); } +type FileUuidDict = Record<string, {submitted: boolean}>; + /** * @param {HTMLElement} dropzoneEl */ @@ -67,7 +71,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) { const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url'); let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event - let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone + let fileUuidDict: FileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone const opts: Record<string, any> = { url: dropzoneEl.getAttribute('data-upload-url'), headers: {'X-Csrf-Token': csrfToken}, @@ -89,7 +93,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) { // "http://localhost:3000/owner/repo/issues/[object%20Event]" // the reason is that the preview "callback(dataURL)" is assign to "img.onerror" then "thumbnail" uses the error object as the dataURL and generates '<img src="[object Event]">' const dzInst = await createDropzone(dropzoneEl, opts); - dzInst.on('success', (file: DropzoneFile & {uuid: string}, resp: any) => { + dzInst.on('success', (file: CustomDropzoneFile, resp: any) => { file.uuid = resp.uuid; fileUuidDict[file.uuid] = {submitted: false}; const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${resp.uuid}`, value: resp.uuid}); @@ -98,7 +102,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) { dzInst.emit(DropzoneCustomEventUploadDone, {file}); }); - dzInst.on('removedfile', async (file: DropzoneFile & {uuid: string}) => { + dzInst.on('removedfile', async (file: CustomDropzoneFile) => { if (disableRemovedfileEvent) return; dzInst.emit(DropzoneCustomEventRemovedFile, {fileUuid: file.uuid}); diff --git a/web_src/js/features/emoji.ts b/web_src/js/features/emoji.ts index 933aa951c5..135620e51e 100644 --- a/web_src/js/features/emoji.ts +++ b/web_src/js/features/emoji.ts @@ -15,13 +15,13 @@ export const emojiKeys = Object.keys(tempMap).sort((a, b) => { return a.localeCompare(b); }); -const emojiMap = {}; +const emojiMap: Record<string, string> = {}; for (const key of emojiKeys) { emojiMap[key] = tempMap[key]; } // retrieve HTML for given emoji name -export function emojiHTML(name) { +export function emojiHTML(name: string) { let inner; if (Object.hasOwn(customEmojis, name)) { inner = `<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`; @@ -33,6 +33,6 @@ export function emojiHTML(name) { } // retrieve string for given emoji name -export function emojiString(name) { +export function emojiString(name: string) { return emojiMap[name] || `:${name}:`; } diff --git a/web_src/js/features/file-fold.ts b/web_src/js/features/file-fold.ts index 6fe068341a..19950d9b9f 100644 --- a/web_src/js/features/file-fold.ts +++ b/web_src/js/features/file-fold.ts @@ -5,15 +5,15 @@ import {svg} from '../svg.ts'; // The fold arrow is the icon displayed on the upper left of the file box, especially intended for components having the 'fold-file' class. // The file content box is the box that should be hidden or shown, especially intended for components having the 'file-content' class. // -export function setFileFolding(fileContentBox, foldArrow, newFold) { +export function setFileFolding(fileContentBox: HTMLElement, foldArrow: HTMLElement, newFold: boolean) { foldArrow.innerHTML = svg(`octicon-chevron-${newFold ? 'right' : 'down'}`, 18); - fileContentBox.setAttribute('data-folded', newFold); + fileContentBox.setAttribute('data-folded', String(newFold)); if (newFold && fileContentBox.getBoundingClientRect().top < 0) { fileContentBox.scrollIntoView(); } } // Like `setFileFolding`, except that it automatically inverts the current file folding state. -export function invertFileFolding(fileContentBox, foldArrow) { +export function invertFileFolding(fileContentBox:HTMLElement, foldArrow: HTMLElement) { setFileFolding(fileContentBox, foldArrow, fileContentBox.getAttribute('data-folded') !== 'true'); } diff --git a/web_src/js/features/heatmap.ts b/web_src/js/features/heatmap.ts index 53eebc93e5..7cec82108b 100644 --- a/web_src/js/features/heatmap.ts +++ b/web_src/js/features/heatmap.ts @@ -7,7 +7,7 @@ export function initHeatmap() { if (!el) return; try { - const heatmap = {}; + const heatmap: Record<string, number> = {}; for (const {contributions, timestamp} of JSON.parse(el.getAttribute('data-heatmap-data'))) { // Convert to user timezone and sum contributions by date const dateStr = new Date(timestamp * 1000).toDateString(); diff --git a/web_src/js/features/imagediff.ts b/web_src/js/features/imagediff.ts index cd61888f83..e62734293a 100644 --- a/web_src/js/features/imagediff.ts +++ b/web_src/js/features/imagediff.ts @@ -3,7 +3,7 @@ import {hideElem, loadElem, queryElemChildren, queryElems} from '../utils/dom.ts import {parseDom} from '../utils.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; -function getDefaultSvgBoundsIfUndefined(text, src) { +function getDefaultSvgBoundsIfUndefined(text: string, src: string) { const defaultSize = 300; const maxSize = 99999; @@ -38,7 +38,7 @@ function getDefaultSvgBoundsIfUndefined(text, src) { return null; } -function createContext(imageAfter, imageBefore) { +function createContext(imageAfter: HTMLImageElement, imageBefore: HTMLImageElement) { const sizeAfter = { width: imageAfter?.width || 0, height: imageAfter?.height || 0, @@ -123,7 +123,7 @@ class ImageDiff { queryElemChildren(containerEl, '.image-diff-tabs', (el) => el.classList.remove('is-loading')); } - initSideBySide(sizes) { + initSideBySide(sizes: Record<string, any>) { let factor = 1; if (sizes.maxSize.width > (this.diffContainerWidth - 24) / 2) { factor = (this.diffContainerWidth - 24) / 2 / sizes.maxSize.width; @@ -176,7 +176,7 @@ class ImageDiff { } } - initSwipe(sizes) { + initSwipe(sizes: Record<string, any>) { let factor = 1; if (sizes.maxSize.width > this.diffContainerWidth - 12) { factor = (this.diffContainerWidth - 12) / sizes.maxSize.width; @@ -215,14 +215,14 @@ class ImageDiff { this.containerEl.querySelector('.swipe-bar').addEventListener('mousedown', (e) => { e.preventDefault(); - this.initSwipeEventListeners(e.currentTarget); + this.initSwipeEventListeners(e.currentTarget as HTMLElement); }); } - initSwipeEventListeners(swipeBar) { - const swipeFrame = swipeBar.parentNode; + initSwipeEventListeners(swipeBar: HTMLElement) { + const swipeFrame = swipeBar.parentNode as HTMLElement; const width = swipeFrame.clientWidth; - const onSwipeMouseMove = (e) => { + const onSwipeMouseMove = (e: MouseEvent) => { e.preventDefault(); const rect = swipeFrame.getBoundingClientRect(); const value = Math.max(0, Math.min(e.clientX - rect.left, width)); @@ -237,7 +237,7 @@ class ImageDiff { document.addEventListener('mouseup', removeEventListeners); } - initOverlay(sizes) { + initOverlay(sizes: Record<string, any>) { let factor = 1; if (sizes.maxSize.width > this.diffContainerWidth - 12) { factor = (this.diffContainerWidth - 12) / sizes.maxSize.width; diff --git a/web_src/js/features/install.ts b/web_src/js/features/install.ts index dddeb1e954..34df4757f9 100644 --- a/web_src/js/features/install.ts +++ b/web_src/js/features/install.ts @@ -12,11 +12,12 @@ export function initInstall() { initPreInstall(); } } + function initPreInstall() { const defaultDbUser = 'gitea'; const defaultDbName = 'gitea'; - const defaultDbHosts = { + const defaultDbHosts: Record<string, string> = { mysql: '127.0.0.1:3306', postgres: '127.0.0.1:5432', mssql: '127.0.0.1:1433', diff --git a/web_src/js/features/org-team.ts b/web_src/js/features/org-team.ts index e160f07bf2..d07818b0ac 100644 --- a/web_src/js/features/org-team.ts +++ b/web_src/js/features/org-team.ts @@ -21,7 +21,7 @@ function initOrgTeamSearchRepoBox() { minCharacters: 2, apiSettings: { url: `${appSubUrl}/repo/search?q={query}&uid=${$searchRepoBox.data('uid')}`, - onResponse(response) { + onResponse(response: any) { const items = []; for (const item of response.data) { items.push({ diff --git a/web_src/js/features/pull-view-file.ts b/web_src/js/features/pull-view-file.ts index 5202d84b28..16ccf00084 100644 --- a/web_src/js/features/pull-view-file.ts +++ b/web_src/js/features/pull-view-file.ts @@ -59,13 +59,13 @@ export function initViewedCheckboxListenerFor() { const fileName = checkbox.getAttribute('name'); // check if the file is in our difftreestore and if we find it -> change the IsViewed status - const fileInPageData = diffTreeStore().files.find((x) => x.Name === fileName); + const fileInPageData = diffTreeStore().files.find((x: Record<string, any>) => x.Name === fileName); if (fileInPageData) { fileInPageData.IsViewed = this.checked; } // Unfortunately, actual forms cause too many problems, hence another approach is needed - const files = {}; + const files: Record<string, boolean> = {}; files[fileName] = this.checked; const data: Record<string, any> = {files}; const headCommitSHA = form.getAttribute('data-headcommit'); @@ -82,13 +82,13 @@ export function initViewedCheckboxListenerFor() { export function initExpandAndCollapseFilesButton() { // expand btn document.querySelector(expandFilesBtnSelector)?.addEventListener('click', () => { - for (const box of document.querySelectorAll('.file-content[data-folded="true"]')) { + for (const box of document.querySelectorAll<HTMLElement>('.file-content[data-folded="true"]')) { setFileFolding(box, box.querySelector('.fold-file'), false); } }); // collapse btn, need to exclude the div of “show more” document.querySelector(collapseFilesBtnSelector)?.addEventListener('click', () => { - for (const box of document.querySelectorAll('.file-content:not([data-folded="true"])')) { + for (const box of document.querySelectorAll<HTMLElement>('.file-content:not([data-folded="true"])')) { if (box.getAttribute('id') === 'diff-incomplete') continue; setFileFolding(box, box.querySelector('.fold-file'), true); } diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts index 90860720e4..fb76d8ed36 100644 --- a/web_src/js/features/repo-common.ts +++ b/web_src/js/features/repo-common.ts @@ -1,4 +1,4 @@ -import {queryElems} from '../utils/dom.ts'; +import {queryElems, type DOMEvent} from '../utils/dom.ts'; import {POST} from '../modules/fetch.ts'; import {showErrorToast} from '../modules/toast.ts'; import {sleep} from '../utils.ts'; @@ -7,10 +7,10 @@ import {createApp} from 'vue'; import {toOriginUrl} from '../utils/url.ts'; import {createTippy} from '../modules/tippy.ts'; -async function onDownloadArchive(e) { +async function onDownloadArchive(e: DOMEvent<MouseEvent>) { e.preventDefault(); // there are many places using the "archive-link", eg: the dropdown on the repo code page, the release list - const el = e.target.closest('a.archive-link[href]'); + const el = e.target.closest<HTMLAnchorElement>('a.archive-link[href]'); const targetLoading = el.closest('.ui.dropdown') ?? el; targetLoading.classList.add('is-loading', 'loading-icon-2px'); try { @@ -107,7 +107,7 @@ export function initRepoCloneButtons() { queryElems(document, '.clone-buttons-combo', initCloneSchemeUrlSelection); } -export async function updateIssuesMeta(url, action, issue_ids, id) { +export async function updateIssuesMeta(url: string, action: string, issue_ids: string, id: string) { try { const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})}); if (!response.ok) { diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index 0cb2e566c0..0dad4da862 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -168,7 +168,7 @@ function onShowMoreFiles() { initDiffHeaderPopup(); } -export async function loadMoreFiles(url) { +export async function loadMoreFiles(url: string) { const target = document.querySelector('a#diff-show-more-files'); if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) { return; diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts index d7097787d2..0f3fb7bbcf 100644 --- a/web_src/js/features/repo-editor.ts +++ b/web_src/js/features/repo-editor.ts @@ -168,7 +168,7 @@ export function initRepoEditor() { silent: true, dirtyClass: dirtyFileClass, fieldSelector: ':input:not(.commit-form-wrapper :input)', - change($form) { + change($form: any) { const dirty = $form[0]?.classList.contains(dirtyFileClass); commitButton.disabled = !dirty; }, diff --git a/web_src/js/features/repo-findfile.ts b/web_src/js/features/repo-findfile.ts index 6500978bc8..59c827126f 100644 --- a/web_src/js/features/repo-findfile.ts +++ b/web_src/js/features/repo-findfile.ts @@ -4,13 +4,15 @@ import {pathEscapeSegments} from '../utils/url.ts'; import {GET} from '../modules/fetch.ts'; const threshold = 50; -let files = []; -let repoFindFileInput, repoFindFileTableBody, repoFindFileNoResult; +let files: Array<string> = []; +let repoFindFileInput: HTMLInputElement; +let repoFindFileTableBody: HTMLElement; +let repoFindFileNoResult: HTMLElement; // return the case-insensitive sub-match result as an array: [unmatched, matched, unmatched, matched, ...] // res[even] is unmatched, res[odd] is matched, see unit tests for examples // argument subLower must be a lower-cased string. -export function strSubMatch(full, subLower) { +export function strSubMatch(full: string, subLower: string) { const res = ['']; let i = 0, j = 0; const fullLower = full.toLowerCase(); @@ -38,7 +40,7 @@ export function strSubMatch(full, subLower) { return res; } -export function calcMatchedWeight(matchResult) { +export function calcMatchedWeight(matchResult: Array<any>) { let weight = 0; for (let i = 0; i < matchResult.length; i++) { if (i % 2 === 1) { // matches are on odd indices, see strSubMatch @@ -49,7 +51,7 @@ export function calcMatchedWeight(matchResult) { return weight; } -export function filterRepoFilesWeighted(files, filter) { +export function filterRepoFilesWeighted(files: Array<string>, filter: string) { let filterResult = []; if (filter) { const filterLower = filter.toLowerCase(); @@ -71,7 +73,7 @@ export function filterRepoFilesWeighted(files, filter) { return filterResult; } -function filterRepoFiles(filter) { +function filterRepoFiles(filter: string) { const treeLink = repoFindFileInput.getAttribute('data-url-tree-link'); repoFindFileTableBody.innerHTML = ''; diff --git a/web_src/js/features/repo-home.ts b/web_src/js/features/repo-home.ts index 763f8e503f..04a1288626 100644 --- a/web_src/js/features/repo-home.ts +++ b/web_src/js/features/repo-home.ts @@ -92,7 +92,7 @@ export function initRepoTopicBar() { onResponse(this: any, res: any) { const formattedResponse = { success: false, - results: [], + results: [] as Array<Record<string, any>>, }; const query = stripTags(this.urlData.query.trim()); let found_query = false; @@ -134,12 +134,12 @@ export function initRepoTopicBar() { return formattedResponse; }, }, - onLabelCreate(value) { + onLabelCreate(value: string) { value = value.toLowerCase().trim(); this.attr('data-value', value).contents().first().replaceWith(value); return fomanticQuery(this); }, - onAdd(addedValue, _addedText, $addedChoice) { + onAdd(addedValue: string, _addedText: any, $addedChoice: any) { addedValue = addedValue.toLowerCase().trim(); $addedChoice[0].setAttribute('data-value', addedValue); $addedChoice[0].setAttribute('data-text', addedValue); diff --git a/web_src/js/features/repo-issue-content.ts b/web_src/js/features/repo-issue-content.ts index 2279c26beb..056b810be8 100644 --- a/web_src/js/features/repo-issue-content.ts +++ b/web_src/js/features/repo-issue-content.ts @@ -33,7 +33,7 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo $fomanticDropdownOptions.dropdown({ showOnFocus: false, allowReselection: true, - async onChange(_value, _text, $item) { + async onChange(_value: string, _text: string, $item: any) { const optionItem = $item.data('option-item'); if (optionItem === 'delete') { if (window.confirm(i18nTextDeleteFromHistoryConfirm)) { @@ -115,7 +115,7 @@ function showContentHistoryMenu(issueBaseUrl: string, elCommentItem: Element, co onHide() { $fomanticDropdown.dropdown('change values', null); }, - onChange(value, itemHtml, $item) { + onChange(value: string, itemHtml: string, $item: any) { if (value && !$item.find('[data-history-is-deleted=1]').length) { showContentHistoryDetail(issueBaseUrl, commentId, value, itemHtml); } diff --git a/web_src/js/features/repo-issue-edit.ts b/web_src/js/features/repo-issue-edit.ts index 38dfea4743..a3a13a156d 100644 --- a/web_src/js/features/repo-issue-edit.ts +++ b/web_src/js/features/repo-issue-edit.ts @@ -2,14 +2,14 @@ import {handleReply} from './repo-issue.ts'; import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; import {POST} from '../modules/fetch.ts'; import {showErrorToast} from '../modules/toast.ts'; -import {hideElem, querySingleVisibleElem, showElem} from '../utils/dom.ts'; +import {hideElem, querySingleVisibleElem, showElem, type DOMEvent} from '../utils/dom.ts'; import {attachRefIssueContextPopup} from './contextpopup.ts'; import {initCommentContent, initMarkupContent} from '../markup/content.ts'; import {triggerUploadStateChanged} from './comp/EditorUpload.ts'; import {convertHtmlToMarkdown} from '../markup/html2markdown.ts'; import {applyAreYouSure, reinitializeAreYouSure} from '../vendor/jquery.are-you-sure.ts'; -async function tryOnEditContent(e) { +async function tryOnEditContent(e: DOMEvent<MouseEvent>) { const clickTarget = e.target.closest('.edit-content'); if (!clickTarget) return; @@ -21,14 +21,14 @@ async function tryOnEditContent(e) { let comboMarkdownEditor : ComboMarkdownEditor; - const cancelAndReset = (e) => { + const cancelAndReset = (e: Event) => { e.preventDefault(); showElem(renderContent); hideElem(editContentZone); comboMarkdownEditor.dropzoneReloadFiles(); }; - const saveAndRefresh = async (e) => { + const saveAndRefresh = async (e: Event) => { e.preventDefault(); // we are already in a form, do not bubble up to the document otherwise there will be other "form submit handlers" // at the moment, the form submit event conflicts with initRepoDiffConversationForm (global '.conversation-holder form' event handler) @@ -60,7 +60,7 @@ async function tryOnEditContent(e) { } else { renderContent.innerHTML = data.content; rawContent.textContent = comboMarkdownEditor.value(); - const refIssues = renderContent.querySelectorAll('p .ref-issue'); + const refIssues = renderContent.querySelectorAll<HTMLElement>('p .ref-issue'); attachRefIssueContextPopup(refIssues); } const content = segment; @@ -125,7 +125,7 @@ function extractSelectedMarkdown(container: HTMLElement) { return convertHtmlToMarkdown(el); } -async function tryOnQuoteReply(e) { +async function tryOnQuoteReply(e: Event) { const clickTarget = (e.target as HTMLElement).closest('.quote-reply'); if (!clickTarget) return; @@ -139,7 +139,7 @@ async function tryOnQuoteReply(e) { let editor; if (clickTarget.classList.contains('quote-reply-diff')) { - const replyBtn = clickTarget.closest('.comment-code-cloud').querySelector('button.comment-form-reply'); + const replyBtn = clickTarget.closest('.comment-code-cloud').querySelector<HTMLElement>('button.comment-form-reply'); editor = await handleReply(replyBtn); } else { // for normal issue/comment page diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts index 74d4362bfd..01d4bb6f78 100644 --- a/web_src/js/features/repo-issue-list.ts +++ b/web_src/js/features/repo-issue-list.ts @@ -7,6 +7,7 @@ import {createSortable} from '../modules/sortable.ts'; import {DELETE, POST} from '../modules/fetch.ts'; import {parseDom} from '../utils.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; +import type {SortableEvent} from 'sortablejs'; function initRepoIssueListCheckboxes() { const issueSelectAll = document.querySelector<HTMLInputElement>('.issue-checkbox-all'); @@ -104,7 +105,7 @@ function initDropdownUserRemoteSearch(el: Element) { $searchDropdown.dropdown('setting', { fullTextSearch: true, selectOnKeydown: false, - action: (_text, value) => { + action: (_text: string, value: string) => { window.location.href = actionJumpUrl.replace('{username}', encodeURIComponent(value)); }, }); @@ -133,7 +134,7 @@ function initDropdownUserRemoteSearch(el: Element) { $searchDropdown.dropdown('setting', 'apiSettings', { cache: false, url: `${searchUrl}&q={query}`, - onResponse(resp) { + onResponse(resp: any) { // the content is provided by backend IssuePosters handler processedResults.length = 0; for (const item of resp.results) { @@ -153,7 +154,7 @@ function initDropdownUserRemoteSearch(el: Element) { const dropdownSetup = {...$searchDropdown.dropdown('internal', 'setup')}; const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates'); $searchDropdown.dropdown('internal', 'setup', dropdownSetup); - dropdownSetup.menu = function (values) { + dropdownSetup.menu = function (values: any) { // remove old dynamic items for (const el of elMenu.querySelectorAll(':scope > .dynamic-item')) { el.remove(); @@ -193,7 +194,7 @@ function initPinRemoveButton() { } } -async function pinMoveEnd(e) { +async function pinMoveEnd(e: SortableEvent) { const url = e.item.getAttribute('data-move-url'); const id = Number(e.item.getAttribute('data-issue-id')); await POST(url, {data: {id, position: e.newIndex + 1}}); diff --git a/web_src/js/features/repo-issue-sidebar-combolist.ts b/web_src/js/features/repo-issue-sidebar-combolist.ts index 24d620547f..8db2f7665f 100644 --- a/web_src/js/features/repo-issue-sidebar-combolist.ts +++ b/web_src/js/features/repo-issue-sidebar-combolist.ts @@ -46,7 +46,7 @@ class IssueSidebarComboList { return Array.from(this.elDropdown.querySelectorAll('.menu > .item.checked'), (el) => el.getAttribute('data-value')); } - updateUiList(changedValues) { + updateUiList(changedValues: Array<string>) { const elEmptyTip = this.elList.querySelector('.item.empty-list'); queryElemChildren(this.elList, '.item:not(.empty-list)', (el) => el.remove()); for (const value of changedValues) { @@ -60,7 +60,7 @@ class IssueSidebarComboList { toggleElem(elEmptyTip, !hasItems); } - async updateToBackend(changedValues) { + async updateToBackend(changedValues: Array<string>) { if (this.updateAlgo === 'diff') { for (const value of this.initialValues) { if (!changedValues.includes(value)) { @@ -93,7 +93,7 @@ class IssueSidebarComboList { } } - async onItemClick(e) { + async onItemClick(e: Event) { const elItem = (e.target as HTMLElement).closest('.item'); if (!elItem) return; e.preventDefault(); diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index d2a89682e8..a0cb875a87 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -32,8 +32,8 @@ export function initRepoIssueSidebarList() { fullTextSearch: true, apiSettings: { url: issueSearchUrl, - onResponse(response) { - const filteredResponse = {success: true, results: []}; + onResponse(response: any) { + const filteredResponse = {success: true, results: [] as Array<Record<string, any>>}; const currIssueId = $('#new-dependency-drop-list').data('issue-id'); // Parse the response from the api to work with our dropdown $.each(response, (_i, issue) => { @@ -247,7 +247,7 @@ export function initRepoPullRequestUpdate() { }); $('.update-button > .dropdown').dropdown({ - onChange(_text, _value, $choice) { + onChange(_text: string, _value: string, $choice: any) { const choiceEl = $choice[0]; const url = choiceEl.getAttribute('data-do'); if (url) { @@ -298,8 +298,8 @@ export function initRepoIssueReferenceRepositorySearch() { .dropdown({ apiSettings: { url: `${appSubUrl}/repo/search?q={query}&limit=20`, - onResponse(response) { - const filteredResponse = {success: true, results: []}; + onResponse(response: any) { + const filteredResponse = {success: true, results: [] as Array<Record<string, any>>}; $.each(response.data, (_r, repo) => { filteredResponse.results.push({ name: htmlEscape(repo.repository.full_name), @@ -310,7 +310,7 @@ export function initRepoIssueReferenceRepositorySearch() { }, cache: false, }, - onChange(_value, _text, $choice) { + onChange(_value: string, _text: string, $choice: any) { const $form = $choice.closest('form'); if (!$form.length) return; @@ -360,7 +360,7 @@ export function initRepoIssueComments() { }); } -export async function handleReply(el) { +export async function handleReply(el: HTMLElement) { const form = el.closest('.comment-code-cloud').querySelector('.comment-form'); const textarea = form.querySelector('textarea'); @@ -379,7 +379,7 @@ export function initRepoPullRequestReview() { const groupID = commentDiv.closest('div[id^="code-comments-"]')?.getAttribute('id'); if (groupID && groupID.startsWith('code-comments-')) { const id = groupID.slice(14); - const ancestorDiffBox = commentDiv.closest('.diff-file-box'); + const ancestorDiffBox = commentDiv.closest<HTMLElement>('.diff-file-box'); hideElem(`#show-outdated-${id}`); showElem(`#code-comments-${id}, #code-preview-${id}, #hide-outdated-${id}`); @@ -589,7 +589,7 @@ export function initRepoIssueBranchSelect() { }); } -async function initSingleCommentEditor($commentForm) { +async function initSingleCommentEditor($commentForm: any) { // pages: // * normal new issue/pr page: no status-button, no comment-button (there is only a normal submit button which can submit empty content) // * issue/pr view page: with comment form, has status-button and comment-button @@ -611,7 +611,7 @@ async function initSingleCommentEditor($commentForm) { syncUiState(); } -function initIssueTemplateCommentEditors($commentForm) { +function initIssueTemplateCommentEditors($commentForm: any) { // pages: // * new issue with issue template const $comboFields = $commentForm.find('.combo-editor-dropzone'); diff --git a/web_src/js/features/repo-migrate.ts b/web_src/js/features/repo-migrate.ts index b75289feec..0788f83215 100644 --- a/web_src/js/features/repo-migrate.ts +++ b/web_src/js/features/repo-migrate.ts @@ -1,11 +1,11 @@ -import {hideElem, showElem} from '../utils/dom.ts'; +import {hideElem, showElem, type DOMEvent} from '../utils/dom.ts'; import {GET, POST} from '../modules/fetch.ts'; export function initRepoMigrationStatusChecker() { const repoMigrating = document.querySelector('#repo_migrating'); if (!repoMigrating) return; - document.querySelector('#repo_migrating_retry')?.addEventListener('click', doMigrationRetry); + document.querySelector<HTMLButtonElement>('#repo_migrating_retry')?.addEventListener('click', doMigrationRetry); const repoLink = repoMigrating.getAttribute('data-migrating-repo-link'); @@ -55,7 +55,7 @@ export function initRepoMigrationStatusChecker() { syncTaskStatus(); // no await } -async function doMigrationRetry(e) { +async function doMigrationRetry(e: DOMEvent<MouseEvent>) { await POST(e.target.getAttribute('data-migrating-task-retry-url')); window.location.reload(); } diff --git a/web_src/js/features/repo-new.ts b/web_src/js/features/repo-new.ts index 8a77a77b4a..f2c5eba62c 100644 --- a/web_src/js/features/repo-new.ts +++ b/web_src/js/features/repo-new.ts @@ -23,7 +23,7 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) { $dropdown.dropdown('setting', { apiSettings: { url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${inputRepoOwnerUid.value}`, - onResponse(response) { + onResponse(response: any) { const results = []; results.push({name: '', value: ''}); // empty item means not using template for (const tmplRepo of response.data) { @@ -66,7 +66,7 @@ export function initRepoNew() { let help = form.querySelector(`.help[data-help-for-repo-name="${CSS.escape(inputRepoName.value)}"]`); if (!help) help = form.querySelector(`.help[data-help-for-repo-name=""]`); showElem(help); - const repoNamePreferPrivate = {'.profile': false, '.profile-private': true}; + const repoNamePreferPrivate: Record<string, boolean> = {'.profile': false, '.profile-private': true}; const preferPrivate = repoNamePreferPrivate[inputRepoName.value]; // inputPrivate might be disabled because site admin "force private" if (preferPrivate !== undefined && !inputPrivate.closest('.disabled, [disabled]')) { diff --git a/web_src/js/features/repo-settings.ts b/web_src/js/features/repo-settings.ts index 7b3ab504cb..b61ef9a153 100644 --- a/web_src/js/features/repo-settings.ts +++ b/web_src/js/features/repo-settings.ts @@ -12,7 +12,7 @@ function initRepoSettingsCollaboration() { for (const dropdownEl of queryElems(document, '.page-content.repository .ui.dropdown.access-mode')) { const textEl = dropdownEl.querySelector(':scope > .text'); $(dropdownEl).dropdown({ - async action(text, value) { + async action(text: string, value: string) { dropdownEl.classList.add('is-loading', 'loading-icon-2px'); const lastValue = dropdownEl.getAttribute('data-last-value'); $(dropdownEl).dropdown('hide'); @@ -53,8 +53,8 @@ function initRepoSettingsSearchTeamBox() { apiSettings: { url: `${appSubUrl}/org/${searchTeamBox.getAttribute('data-org-name')}/teams/-/search?q={query}`, headers: {'X-Csrf-Token': csrfToken}, - onResponse(response) { - const items = []; + onResponse(response: any) { + const items: Array<Record<string, any>> = []; $.each(response.data, (_i, item) => { items.push({ title: item.name, diff --git a/web_src/js/features/repo-wiki.ts b/web_src/js/features/repo-wiki.ts index 484c628f9f..9ffa8a3275 100644 --- a/web_src/js/features/repo-wiki.ts +++ b/web_src/js/features/repo-wiki.ts @@ -70,7 +70,7 @@ async function initRepoWikiFormEditor() { }); } -function collapseWikiTocForMobile(collapse) { +function collapseWikiTocForMobile(collapse: boolean) { if (collapse) { document.querySelector('.wiki-content-toc details')?.removeAttribute('open'); } diff --git a/web_src/js/features/stopwatch.ts b/web_src/js/features/stopwatch.ts index 46168b2cd7..a5cd5ae7c4 100644 --- a/web_src/js/features/stopwatch.ts +++ b/web_src/js/features/stopwatch.ts @@ -38,7 +38,7 @@ export function initStopwatch() { } let usingPeriodicPoller = false; - const startPeriodicPoller = (timeout) => { + const startPeriodicPoller = (timeout: number) => { if (timeout <= 0 || !Number.isFinite(timeout)) return; usingPeriodicPoller = true; setTimeout(() => updateStopwatchWithCallback(startPeriodicPoller, timeout), timeout); @@ -103,7 +103,7 @@ export function initStopwatch() { startPeriodicPoller(notificationSettings.MinTimeout); } -async function updateStopwatchWithCallback(callback, timeout) { +async function updateStopwatchWithCallback(callback: (timeout: number) => void, timeout: number) { const isSet = await updateStopwatch(); if (!isSet) { @@ -125,7 +125,7 @@ async function updateStopwatch() { return updateStopwatchData(data); } -function updateStopwatchData(data) { +function updateStopwatchData(data: any) { const watch = data[0]; const btnEls = document.querySelectorAll('.active-stopwatch'); if (!watch) { diff --git a/web_src/js/features/tablesort.ts b/web_src/js/features/tablesort.ts index 15ea358fa3..0648ffd067 100644 --- a/web_src/js/features/tablesort.ts +++ b/web_src/js/features/tablesort.ts @@ -9,7 +9,7 @@ export function initTableSort() { } } -function tableSort(normSort, revSort, isDefault) { +function tableSort(normSort: string, revSort: string, isDefault: string) { if (!normSort) return false; if (!revSort) revSort = ''; diff --git a/web_src/js/features/tribute.ts b/web_src/js/features/tribute.ts index fa65bcbb28..de1c3e97cd 100644 --- a/web_src/js/features/tribute.ts +++ b/web_src/js/features/tribute.ts @@ -1,14 +1,16 @@ import {emojiKeys, emojiHTML, emojiString} from './emoji.ts'; import {htmlEscape} from 'escape-goat'; -function makeCollections({mentions, emoji}) { - const collections = []; +type TributeItem = Record<string, any>; - if (emoji) { - collections.push({ +export async function attachTribute(element: HTMLElement) { + const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs'); + + const collections = [ + { // emojis trigger: ':', requireLeadingSpace: true, - values: (query, cb) => { + values: (query: string, cb: (matches: Array<string>) => void) => { const matches = []; for (const name of emojiKeys) { if (name.includes(query)) { @@ -18,22 +20,18 @@ function makeCollections({mentions, emoji}) { } cb(matches); }, - lookup: (item) => item, - selectTemplate: (item) => { + lookup: (item: TributeItem) => item, + selectTemplate: (item: TributeItem) => { if (item === undefined) return null; return emojiString(item.original); }, - menuItemTemplate: (item) => { + menuItemTemplate: (item: TributeItem) => { return `<div class="tribute-item">${emojiHTML(item.original)}<span>${htmlEscape(item.original)}</span></div>`; }, - }); - } - - if (mentions) { - collections.push({ + }, { // mentions values: window.config.mentionValues ?? [], requireLeadingSpace: true, - menuItemTemplate: (item) => { + menuItemTemplate: (item: TributeItem) => { return ` <div class="tribute-item"> <img src="${htmlEscape(item.original.avatar)}" width="21" height="21"/> @@ -42,15 +40,9 @@ function makeCollections({mentions, emoji}) { </div> `; }, - }); - } + }, + ]; - return collections; -} - -export async function attachTribute(element, {mentions, emoji}) { - const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs'); - const collections = makeCollections({mentions, emoji}); // @ts-expect-error TS2351: This expression is not constructable (strange, why) const tribute = new Tribute({collection: collections, noMatchTemplate: ''}); tribute.attach(element); diff --git a/web_src/js/features/user-auth-webauthn.ts b/web_src/js/features/user-auth-webauthn.ts index 70516c280d..b9ab2e2088 100644 --- a/web_src/js/features/user-auth-webauthn.ts +++ b/web_src/js/features/user-auth-webauthn.ts @@ -114,7 +114,7 @@ async function login2FA() { } } -async function verifyAssertion(assertedCredential) { +async function verifyAssertion(assertedCredential: any) { // TODO: Credential type does not work // Move data into Arrays in case it is super long const authData = new Uint8Array(assertedCredential.response.authenticatorData); const clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON); @@ -148,7 +148,7 @@ async function verifyAssertion(assertedCredential) { window.location.href = reply?.redirect ?? `${appSubUrl}/`; } -async function webauthnRegistered(newCredential) { +async function webauthnRegistered(newCredential: any) { // TODO: Credential type does not work const attestationObject = new Uint8Array(newCredential.response.attestationObject); const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); const rawId = new Uint8Array(newCredential.rawId); diff --git a/web_src/js/markup/asciicast.ts b/web_src/js/markup/asciicast.ts index 97b18743a1..9baae6ba85 100644 --- a/web_src/js/markup/asciicast.ts +++ b/web_src/js/markup/asciicast.ts @@ -3,6 +3,7 @@ export async function renderAsciicast() { if (!els.length) return; const [player] = await Promise.all([ + // @ts-expect-error: module exports no types import(/* webpackChunkName: "asciinema-player" */'asciinema-player'), import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'), ]); diff --git a/web_src/js/markup/html2markdown.ts b/web_src/js/markup/html2markdown.ts index fc2083e86d..8c2d2f8c86 100644 --- a/web_src/js/markup/html2markdown.ts +++ b/web_src/js/markup/html2markdown.ts @@ -1,7 +1,9 @@ import {htmlEscape} from 'escape-goat'; +type Processor = (el: HTMLElement) => string | HTMLElement | void; + type Processors = { - [tagName: string]: (el: HTMLElement) => string | HTMLElement | void; + [tagName: string]: Processor; } type ProcessorContext = { @@ -11,7 +13,7 @@ type ProcessorContext = { } function prepareProcessors(ctx:ProcessorContext): Processors { - const processors = { + const processors: Processors = { H1(el: HTMLElement) { const level = parseInt(el.tagName.slice(1)); el.textContent = `${'#'.repeat(level)} ${el.textContent.trim()}`; diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index e479c79ee6..8736e041df 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -38,7 +38,7 @@ function ariaDropdownFn(this: any, ...args: Parameters<FomanticInitFunction>) { // the elements inside the dropdown menu item should not be focusable, the focus should always be on the dropdown primary element. function updateMenuItem(dropdown: HTMLElement, item: HTMLElement) { if (!item.id) item.id = generateAriaId(); - item.setAttribute('role', dropdown[ariaPatchKey].listItemRole); + item.setAttribute('role', (dropdown as any)[ariaPatchKey].listItemRole); item.setAttribute('tabindex', '-1'); for (const el of item.querySelectorAll('a, input, button')) el.setAttribute('tabindex', '-1'); } @@ -61,7 +61,7 @@ function updateSelectionLabel(label: HTMLElement) { } } -function processMenuItems($dropdown, dropdownCall) { +function processMenuItems($dropdown: any, dropdownCall: any) { const hideEmptyDividers = dropdownCall('setting', 'hideDividers') === 'empty'; const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu'); if (hideEmptyDividers) hideScopedEmptyDividers(itemsMenu); @@ -143,7 +143,7 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men $(menu).find('> .item').each((_, item) => updateMenuItem(dropdown, item)); // this role could only be changed after its content is ready, otherwise some browsers+readers (like Chrome+AppleVoice) crash - menu.setAttribute('role', dropdown[ariaPatchKey].listPopupRole); + menu.setAttribute('role', (dropdown as any)[ariaPatchKey].listPopupRole); // prepare selection label items for (const label of dropdown.querySelectorAll<HTMLElement>('.ui.label')) { @@ -151,8 +151,8 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men } // make the primary element (focusable) aria-friendly - focusable.setAttribute('role', focusable.getAttribute('role') ?? dropdown[ariaPatchKey].focusableRole); - focusable.setAttribute('aria-haspopup', dropdown[ariaPatchKey].listPopupRole); + focusable.setAttribute('role', focusable.getAttribute('role') ?? (dropdown as any)[ariaPatchKey].focusableRole); + focusable.setAttribute('aria-haspopup', (dropdown as any)[ariaPatchKey].listPopupRole); focusable.setAttribute('aria-controls', menu.id); focusable.setAttribute('aria-expanded', 'false'); @@ -164,7 +164,7 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men } function attachInit(dropdown: HTMLElement) { - dropdown[ariaPatchKey] = {}; + (dropdown as any)[ariaPatchKey] = {}; if (dropdown.classList.contains('custom')) return; // Dropdown has 2 different focusing behaviors @@ -204,9 +204,9 @@ function attachInit(dropdown: HTMLElement) { // Since #19861 we have prepared the "combobox" solution, but didn't get enough time to put it into practice and test before. const isComboBox = dropdown.querySelectorAll('input').length > 0; - dropdown[ariaPatchKey].focusableRole = isComboBox ? 'combobox' : 'menu'; - dropdown[ariaPatchKey].listPopupRole = isComboBox ? 'listbox' : ''; - dropdown[ariaPatchKey].listItemRole = isComboBox ? 'option' : 'menuitem'; + (dropdown as any)[ariaPatchKey].focusableRole = isComboBox ? 'combobox' : 'menu'; + (dropdown as any)[ariaPatchKey].listPopupRole = isComboBox ? 'listbox' : ''; + (dropdown as any)[ariaPatchKey].listItemRole = isComboBox ? 'option' : 'menuitem'; attachDomEvents(dropdown, focusable, menu); attachStaticElements(dropdown, focusable, menu); @@ -229,7 +229,7 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT // if the popup is visible and has an active/selected item, use its id as aria-activedescendant if (menuVisible) { focusable.setAttribute('aria-activedescendant', active.id); - } else if (dropdown[ariaPatchKey].listPopupRole === 'menu') { + } else if ((dropdown as any)[ariaPatchKey].listPopupRole === 'menu') { // for menu, when the popup is hidden, no need to keep the aria-activedescendant, and clear the active/selected item focusable.removeAttribute('aria-activedescendant'); active.classList.remove('active', 'selected'); @@ -253,7 +253,7 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT // when the popup is hiding, it's better to have a small "delay", because there is a Fomantic UI animation // without the delay for hiding, the UI will be somewhat laggy and sometimes may get stuck in the animation. const deferredRefreshAriaActiveItem = (delay = 0) => { setTimeout(refreshAriaActiveItem, delay) }; - dropdown[ariaPatchKey].deferredRefreshAriaActiveItem = deferredRefreshAriaActiveItem; + (dropdown as any)[ariaPatchKey].deferredRefreshAriaActiveItem = deferredRefreshAriaActiveItem; dropdown.addEventListener('keyup', (e) => { if (e.key.startsWith('Arrow')) deferredRefreshAriaActiveItem(); }); // if the dropdown has been opened by focus, do not trigger the next click event again. @@ -363,7 +363,7 @@ function onResponseKeepSelectedItem(dropdown: typeof $|HTMLElement, selectedValu // then the dropdown only shows other items and will select another (wrong) one. // It can't be easily fix by using setTimeout(patch, 0) in `onResponse` because the `onResponse` is called before another `setTimeout(..., timeLeft)` // Fortunately, the "timeLeft" is controlled by "loadingDuration" which is always zero at the moment, so we can use `setTimeout(..., 10)` - const elDropdown = (dropdown instanceof HTMLElement) ? dropdown : dropdown[0]; + const elDropdown = (dropdown instanceof HTMLElement) ? dropdown : (dropdown as any)[0]; setTimeout(() => { queryElems(elDropdown, `.menu .item[data-value="${CSS.escape(selectedValue)}"].filtered`, (el) => el.classList.remove('filtered')); $(elDropdown).dropdown('set selected', selectedValue ?? ''); diff --git a/web_src/js/standalone/devtest.ts b/web_src/js/standalone/devtest.ts index 3489697a2f..e6baf6c9ce 100644 --- a/web_src/js/standalone/devtest.ts +++ b/web_src/js/standalone/devtest.ts @@ -1,7 +1,7 @@ import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.ts'; function initDevtestToast() { - const levelMap = {info: showInfoToast, warning: showWarningToast, error: showErrorToast}; + const levelMap: Record<string, any> = {info: showInfoToast, warning: showWarningToast, error: showErrorToast}; for (const el of document.querySelectorAll('.toast-test-button')) { el.addEventListener('click', () => { const level = el.getAttribute('data-toast-level'); diff --git a/web_src/js/svg.ts b/web_src/js/svg.ts index 1dd6922abb..b193afb255 100644 --- a/web_src/js/svg.ts +++ b/web_src/js/svg.ts @@ -208,7 +208,7 @@ export const SvgIcon = defineComponent({ let {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name); // https://vuejs.org/guide/extras/render-function.html#creating-vnodes // the `^` is used for attr, set SVG attributes like 'width', `aria-hidden`, `viewBox`, etc - const attrs = {}; + const attrs: Record<string, any> = {}; for (const attr of svgOuter.attributes) { if (attr.name === 'class') continue; attrs[`^${attr.name}`] = attr.value; diff --git a/web_src/js/types.ts b/web_src/js/types.ts index e972994928..1b5e652f66 100644 --- a/web_src/js/types.ts +++ b/web_src/js/types.ts @@ -22,6 +22,8 @@ export type Config = { i18n: Record<string, string>, } +export type IntervalId = ReturnType<typeof setInterval>; + export type Intent = 'error' | 'warning' | 'info'; export type RequestData = string | FormData | URLSearchParams | Record<string, any>; diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts index 86bdd3790e..54f59a2c03 100644 --- a/web_src/js/utils.ts +++ b/web_src/js/utils.ts @@ -166,10 +166,10 @@ export function sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } -export function isImageFile({name, type}: {name: string, type?: string}): boolean { +export function isImageFile({name, type}: {name?: string, type?: string}): boolean { return /\.(avif|jpe?g|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/'); } -export function isVideoFile({name, type}: {name: string, type?: string}): boolean { +export function isVideoFile({name, type}: {name?: string, type?: string}): boolean { return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/'); } diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index e24cb29bac..603f967b34 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -255,12 +255,12 @@ export function loadElem(el: LoadableElement, src: string) { // it can't use other transparent polyfill patches because PaleMoon also doesn't support "addEventListener(capture)" const needSubmitEventPolyfill = typeof SubmitEvent === 'undefined'; -export function submitEventSubmitter(e) { +export function submitEventSubmitter(e: any) { e = e.originalEvent ?? e; // if the event is wrapped by jQuery, use "originalEvent", otherwise, use the event itself return needSubmitEventPolyfill ? (e.target._submitter || null) : e.submitter; } -function submitEventPolyfillListener(e) { +function submitEventPolyfillListener(e: DOMEvent<Event>) { const form = e.target.closest('form'); if (!form) return; form._submitter = e.target.closest('button:not([type]), button[type="submit"], input[type="submit"]'); diff --git a/web_src/js/utils/image.test.ts b/web_src/js/utils/image.test.ts index da0605f1d0..49856c891c 100644 --- a/web_src/js/utils/image.test.ts +++ b/web_src/js/utils/image.test.ts @@ -4,7 +4,7 @@ const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A=='; const pngEmpty = 'data:image/png;base64,'; -async function dataUriToBlob(datauri) { +async function dataUriToBlob(datauri: string) { return await (await globalThis.fetch(datauri)).blob(); } diff --git a/web_src/js/utils/time.ts b/web_src/js/utils/time.ts index 6951ebfedb..c63498345f 100644 --- a/web_src/js/utils/time.ts +++ b/web_src/js/utils/time.ts @@ -54,7 +54,7 @@ export type DayDataObject = { } export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayDataObject): DayData[] { - const result = {}; + const result: Record<string, any> = {}; for (const startDay of startDays) { result[startDay] = data[startDay] || {'week': startDay, 'additions': 0, 'deletions': 0, 'commits': 0}; diff --git a/web_src/js/webcomponents/absolute-date.ts b/web_src/js/webcomponents/absolute-date.ts index 8eb1c3e37e..23a8606673 100644 --- a/web_src/js/webcomponents/absolute-date.ts +++ b/web_src/js/webcomponents/absolute-date.ts @@ -15,7 +15,7 @@ window.customElements.define('absolute-date', class extends HTMLElement { initialized = false; update = () => { - const opt: Intl.DateTimeFormatOptions = {}; + const opt: Record<string, string> = {}; for (const attr of ['year', 'month', 'weekday', 'day']) { if (this.getAttribute(attr)) opt[attr] = this.getAttribute(attr); } |