mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 02:46:04 +01:00 
			
		
		
		
	Enable Typescript noImplicitAny (#33322)
				
					
				
			Enable `noImplicitAny` and fix all issues. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -1,12 +1,13 @@ | |||||||
| import {expect} from '@playwright/test'; | import {expect} from '@playwright/test'; | ||||||
| import {env} from 'node:process'; | import {env} from 'node:process'; | ||||||
|  | import type {Browser, Page, WorkerInfo} from '@playwright/test'; | ||||||
|  |  | ||||||
| const ARTIFACTS_PATH = `tests/e2e/test-artifacts`; | const ARTIFACTS_PATH = `tests/e2e/test-artifacts`; | ||||||
| const LOGIN_PASSWORD = 'password'; | const LOGIN_PASSWORD = 'password'; | ||||||
|  |  | ||||||
| // log in user and store session info. This should generally be | // log in user and store session info. This should generally be | ||||||
| //  run in test.beforeAll(), then the session can be loaded in tests. | //  run in test.beforeAll(), then the session can be loaded in tests. | ||||||
| export async function login_user(browser, workerInfo, user) { | export async function login_user(browser: Browser, workerInfo: WorkerInfo, user: string) { | ||||||
|   // Set up a new context |   // Set up a new context | ||||||
|   const context = await browser.newContext(); |   const context = await browser.newContext(); | ||||||
|   const page = await context.newPage(); |   const page = await context.newPage(); | ||||||
| @@ -17,8 +18,8 @@ export async function login_user(browser, workerInfo, user) { | |||||||
|   expect(response?.status()).toBe(200); // Status OK |   expect(response?.status()).toBe(200); // Status OK | ||||||
|  |  | ||||||
|   // Fill out form |   // Fill out form | ||||||
|   await page.type('input[name=user_name]', user); |   await page.locator('input[name=user_name]').fill(user); | ||||||
|   await page.type('input[name=password]', LOGIN_PASSWORD); |   await page.locator('input[name=password]').fill(LOGIN_PASSWORD); | ||||||
|   await page.click('form button.ui.primary.button:visible'); |   await page.click('form button.ui.primary.button:visible'); | ||||||
|  |  | ||||||
|   await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle |   await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle | ||||||
| @@ -31,7 +32,7 @@ export async function login_user(browser, workerInfo, user) { | |||||||
|   return context; |   return context; | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function load_logged_in_context(browser, workerInfo, user) { | export async function load_logged_in_context(browser: Browser, workerInfo: WorkerInfo, user: string) { | ||||||
|   let context; |   let context; | ||||||
|   try { |   try { | ||||||
|     context = await browser.newContext({storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`}); |     context = await browser.newContext({storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`}); | ||||||
| @@ -43,7 +44,7 @@ export async function load_logged_in_context(browser, workerInfo, user) { | |||||||
|   return context; |   return context; | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function save_visual(page) { | export async function save_visual(page: Page) { | ||||||
|   // Optionally include visual testing |   // Optionally include visual testing | ||||||
|   if (env.VISUAL_TEST) { |   if (env.VISUAL_TEST) { | ||||||
|     await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle |     await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ | |||||||
|     "stripInternal": true, |     "stripInternal": true, | ||||||
|     "strict": false, |     "strict": false, | ||||||
|     "strictFunctionTypes": true, |     "strictFunctionTypes": true, | ||||||
|  |     "noImplicitAny": true, | ||||||
|     "noImplicitThis": true, |     "noImplicitThis": true, | ||||||
|     "noUnusedLocals": true, |     "noUnusedLocals": true, | ||||||
|     "noUnusedParameters": true, |     "noUnusedParameters": true, | ||||||
|   | |||||||
| @@ -130,12 +130,12 @@ export default defineComponent({ | |||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   methods: { |   methods: { | ||||||
|     changeTab(t) { |     changeTab(tab: string) { | ||||||
|       this.tab = t; |       this.tab = tab; | ||||||
|       this.updateHistory(); |       this.updateHistory(); | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     changeReposFilter(filter) { |     changeReposFilter(filter: string) { | ||||||
|       this.reposFilter = filter; |       this.reposFilter = filter; | ||||||
|       this.repos = []; |       this.repos = []; | ||||||
|       this.page = 1; |       this.page = 1; | ||||||
| @@ -218,7 +218,7 @@ export default defineComponent({ | |||||||
|       this.searchRepos(); |       this.searchRepos(); | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     changePage(page) { |     changePage(page: number) { | ||||||
|       this.page = page; |       this.page = page; | ||||||
|       if (this.page > this.finalPage) { |       if (this.page > this.finalPage) { | ||||||
|         this.page = this.finalPage; |         this.page = this.finalPage; | ||||||
| @@ -256,7 +256,7 @@ export default defineComponent({ | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (searchedURL === this.searchURL) { |       if (searchedURL === this.searchURL) { | ||||||
|         this.repos = json.data.map((webSearchRepo) => { |         this.repos = json.data.map((webSearchRepo: any) => { | ||||||
|           return { |           return { | ||||||
|             ...webSearchRepo.repository, |             ...webSearchRepo.repository, | ||||||
|             latest_commit_status_state: webSearchRepo.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status |             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, |             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') { |         if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') { | ||||||
|           this.reposTotalCount = count; |           this.reposTotalCount = count; | ||||||
|         } |         } | ||||||
| @@ -275,7 +275,7 @@ export default defineComponent({ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     repoIcon(repo) { |     repoIcon(repo: any) { | ||||||
|       if (repo.fork) { |       if (repo.fork) { | ||||||
|         return 'octicon-repo-forked'; |         return 'octicon-repo-forked'; | ||||||
|       } else if (repo.mirror) { |       } else if (repo.mirror) { | ||||||
| @@ -298,7 +298,7 @@ export default defineComponent({ | |||||||
|       return commitStatus[status].color; |       return commitStatus[status].color; | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     reposFilterKeyControl(e) { |     reposFilterKeyControl(e: KeyboardEvent) { | ||||||
|       switch (e.key) { |       switch (e.key) { | ||||||
|         case 'Enter': |         case 'Enter': | ||||||
|           document.querySelector<HTMLAnchorElement>('.repo-owner-name-list li.active a')?.click(); |           document.querySelector<HTMLAnchorElement>('.repo-owner-name-list li.active a')?.click(); | ||||||
|   | |||||||
| @@ -4,6 +4,22 @@ import {SvgIcon} from '../svg.ts'; | |||||||
| import {GET} from '../modules/fetch.ts'; | import {GET} from '../modules/fetch.ts'; | ||||||
| import {generateAriaId} from '../modules/fomantic/base.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({ | export default defineComponent({ | ||||||
|   components: {SvgIcon}, |   components: {SvgIcon}, | ||||||
|   data: () => { |   data: () => { | ||||||
| @@ -16,9 +32,9 @@ export default defineComponent({ | |||||||
|       locale: { |       locale: { | ||||||
|         filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'), |         filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'), | ||||||
|       } as Record<string, string>, |       } as Record<string, string>, | ||||||
|       commits: [], |       commits: [] as Array<Commit>, | ||||||
|       hoverActivated: false, |       hoverActivated: false, | ||||||
|       lastReviewCommitSha: null, |       lastReviewCommitSha: '', | ||||||
|       uniqueIdMenu: generateAriaId(), |       uniqueIdMenu: generateAriaId(), | ||||||
|       uniqueIdShowAll: generateAriaId(), |       uniqueIdShowAll: generateAriaId(), | ||||||
|     }; |     }; | ||||||
| @@ -71,7 +87,7 @@ export default defineComponent({ | |||||||
|       if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { |       if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { | ||||||
|         const item = document.activeElement; // try to highlight the selected commits |         const item = document.activeElement; // try to highlight the selected commits | ||||||
|         const commitIdx = item?.matches('.item') ? item.getAttribute('data-commit-idx') : null; |         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) { |     onKeyUp(event: KeyboardEvent) { | ||||||
| @@ -87,7 +103,7 @@ export default defineComponent({ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     highlight(commit) { |     highlight(commit: Commit) { | ||||||
|       if (!this.hoverActivated) return; |       if (!this.hoverActivated) return; | ||||||
|       const indexSelected = this.commits.findIndex((x) => x.selected); |       const indexSelected = this.commits.findIndex((x) => x.selected); | ||||||
|       const indexCurrentElem = this.commits.findIndex((x) => x.id === commit.id); |       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 */ |     /** Load the commits to show in this dropdown */ | ||||||
|     async fetchCommits() { |     async fetchCommits() { | ||||||
|       const resp = await GET(`${this.issueLink}/commits/list`); |       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) => { |       this.commits.push(...results.commits.map((x) => { | ||||||
|         x.hovered = false; |         x.hovered = false; | ||||||
|         return x; |         return x; | ||||||
| @@ -166,7 +183,7 @@ export default defineComponent({ | |||||||
|      * the diff from beginning of PR up to the second clicked commit is |      * the diff from beginning of PR up to the second clicked commit is | ||||||
|      * opened |      * opened | ||||||
|      */ |      */ | ||||||
|     commitClickedShift(commit) { |     commitClickedShift(commit: Commit) { | ||||||
|       this.hoverActivated = !this.hoverActivated; |       this.hoverActivated = !this.hoverActivated; | ||||||
|       commit.selected = true; |       commit.selected = true; | ||||||
|       // Second click -> determine our range and open links accordingly |       // Second click -> determine our range and open links accordingly | ||||||
|   | |||||||
| @@ -18,14 +18,14 @@ function toggleFileList() { | |||||||
| } | } | ||||||
|  |  | ||||||
| function diffTypeToString(pType: number) { | function diffTypeToString(pType: number) { | ||||||
|   const diffTypes = { |   const diffTypes: Record<string, string> = { | ||||||
|     1: 'add', |     '1': 'add', | ||||||
|     2: 'modify', |     '2': 'modify', | ||||||
|     3: 'del', |     '3': 'del', | ||||||
|     4: 'rename', |     '4': 'rename', | ||||||
|     5: 'copy', |     '5': 'copy', | ||||||
|   }; |   }; | ||||||
|   return diffTypes[pType]; |   return diffTypes[String(pType)]; | ||||||
| } | } | ||||||
|  |  | ||||||
| function diffStatsWidth(adds: number, dels: number) { | function diffStatsWidth(adds: number, dels: number) { | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import DiffFileTreeItem from './DiffFileTreeItem.vue'; | import DiffFileTreeItem, {type Item} from './DiffFileTreeItem.vue'; | ||||||
| import {loadMoreFiles} from '../features/repo-diff.ts'; | import {loadMoreFiles} from '../features/repo-diff.ts'; | ||||||
| import {toggleElem} from '../utils/dom.ts'; | import {toggleElem} from '../utils/dom.ts'; | ||||||
| import {diffTreeStore} from '../modules/stores.ts'; | import {diffTreeStore} from '../modules/stores.ts'; | ||||||
| @@ -11,7 +11,7 @@ const LOCAL_STORAGE_KEY = 'diff_file_tree_visible'; | |||||||
| const store = diffTreeStore(); | const store = diffTreeStore(); | ||||||
|  |  | ||||||
| const fileTree = computed(() => { | const fileTree = computed(() => { | ||||||
|   const result = []; |   const result: Array<Item> = []; | ||||||
|   for (const file of store.files) { |   for (const file of store.files) { | ||||||
|     // Split file into directories |     // Split file into directories | ||||||
|     const splits = file.Name.split('/'); |     const splits = file.Name.split('/'); | ||||||
| @@ -24,15 +24,10 @@ const fileTree = computed(() => { | |||||||
|       if (index === splits.length) { |       if (index === splits.length) { | ||||||
|         isFile = true; |         isFile = true; | ||||||
|       } |       } | ||||||
|       let newParent = { |       let newParent: Item = { | ||||||
|         name: split, |         name: split, | ||||||
|         children: [], |         children: [], | ||||||
|         isFile, |         isFile, | ||||||
|       } as { |  | ||||||
|         name: string, |  | ||||||
|         children: any[], |  | ||||||
|         isFile: boolean, |  | ||||||
|         file?: any, |  | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       if (isFile === true) { |       if (isFile === true) { | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
| import {SvgIcon} from '../svg.ts'; | import {SvgIcon, type SvgName} from '../svg.ts'; | ||||||
| import {diffTreeStore} from '../modules/stores.ts'; | import {diffTreeStore} from '../modules/stores.ts'; | ||||||
| import {ref} from 'vue'; | import {ref} from 'vue'; | ||||||
|  |  | ||||||
| @@ -11,7 +11,7 @@ type File = { | |||||||
|   IsSubmodule: boolean; |   IsSubmodule: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| type Item = { | export type Item = { | ||||||
|   name: string; |   name: string; | ||||||
|   isFile: boolean; |   isFile: boolean; | ||||||
|   file?: File; |   file?: File; | ||||||
| @@ -26,14 +26,14 @@ const store = diffTreeStore(); | |||||||
| const collapsed = ref(false); | const collapsed = ref(false); | ||||||
|  |  | ||||||
| function getIconForDiffType(pType: number) { | function getIconForDiffType(pType: number) { | ||||||
|   const diffTypes = { |   const diffTypes: Record<string, {name: SvgName, classes: Array<string>}> = { | ||||||
|     1: {name: 'octicon-diff-added', classes: ['text', 'green']}, |     '1': {name: 'octicon-diff-added', classes: ['text', 'green']}, | ||||||
|     2: {name: 'octicon-diff-modified', classes: ['text', 'yellow']}, |     '2': {name: 'octicon-diff-modified', classes: ['text', 'yellow']}, | ||||||
|     3: {name: 'octicon-diff-removed', classes: ['text', 'red']}, |     '3': {name: 'octicon-diff-removed', classes: ['text', 'red']}, | ||||||
|     4: {name: 'octicon-diff-renamed', classes: ['text', 'teal']}, |     '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 |     '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) { | function fileIcon(file: File) { | ||||||
|   | |||||||
| @@ -36,17 +36,17 @@ const forceMerge = computed(() => { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| watch(mergeStyle, (val) => { | 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]')) { |   for (const elem of document.querySelectorAll('[data-pull-merge-style]')) { | ||||||
|     toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val); |     toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val); | ||||||
|   } |   } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| onMounted(() => { | 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; |   let mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name; | ||||||
|   if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed)?.name; |   if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed)?.name; | ||||||
|   switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow); |   switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow); | ||||||
|  |  | ||||||
|   document.addEventListener('mouseup', hideMergeStyleMenu); |   document.addEventListener('mouseup', hideMergeStyleMenu); | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import {createElementFromAttrs, toggleElem} from '../utils/dom.ts'; | |||||||
| import {formatDatetime} from '../utils/time.ts'; | import {formatDatetime} from '../utils/time.ts'; | ||||||
| import {renderAnsi} from '../render/ansi.ts'; | import {renderAnsi} from '../render/ansi.ts'; | ||||||
| import {POST, DELETE} from '../modules/fetch.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" | // 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'; | type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked'; | ||||||
| @@ -24,6 +25,20 @@ type LogLineCommand = { | |||||||
|   prefix: string, |   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 { | function parseLineCommand(line: LogLine): LogLineCommand | null { | ||||||
|   for (const prefix of LogLinePrefixesGroup) { |   for (const prefix of LogLinePrefixesGroup) { | ||||||
|     if (line.message.startsWith(prefix)) { |     if (line.message.startsWith(prefix)) { | ||||||
| @@ -77,7 +92,7 @@ export default defineComponent({ | |||||||
|       default: '', |       default: '', | ||||||
|     }, |     }, | ||||||
|     locale: { |     locale: { | ||||||
|       type: Object as PropType<Record<string, string>>, |       type: Object as PropType<Record<string, any>>, | ||||||
|       default: null, |       default: null, | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| @@ -86,10 +101,10 @@ export default defineComponent({ | |||||||
|     const {autoScroll, expandRunning} = getLocaleStorageOptions(); |     const {autoScroll, expandRunning} = getLocaleStorageOptions(); | ||||||
|     return { |     return { | ||||||
|       // internal state |       // internal state | ||||||
|       loadingAbortController: null, |       loadingAbortController: null as AbortController | null, | ||||||
|       intervalID: null, |       intervalID: null as IntervalId | null, | ||||||
|       currentJobStepsStates: [], |       currentJobStepsStates: [] as Array<Record<string, any>>, | ||||||
|       artifacts: [], |       artifacts: [] as Array<Record<string, any>>, | ||||||
|       onHoverRerunIndex: -1, |       onHoverRerunIndex: -1, | ||||||
|       menuVisible: false, |       menuVisible: false, | ||||||
|       isFullScreen: false, |       isFullScreen: false, | ||||||
| @@ -122,7 +137,7 @@ export default defineComponent({ | |||||||
|           //   canRerun: false, |           //   canRerun: false, | ||||||
|           //   duration: '', |           //   duration: '', | ||||||
|           // }, |           // }, | ||||||
|         ], |         ] as Array<Job>, | ||||||
|         commit: { |         commit: { | ||||||
|           localeCommit: '', |           localeCommit: '', | ||||||
|           localePushedBy: '', |           localePushedBy: '', | ||||||
| @@ -148,7 +163,7 @@ export default defineComponent({ | |||||||
|           //   duration: '', |           //   duration: '', | ||||||
|           //   status: '', |           //   status: '', | ||||||
|           // } |           // } | ||||||
|         ], |         ] as Array<Step>, | ||||||
|       }, |       }, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
| @@ -194,7 +209,7 @@ export default defineComponent({ | |||||||
|  |  | ||||||
|     // get the job step logs container ('.job-step-logs') |     // get the job step logs container ('.job-step-logs') | ||||||
|     getJobStepLogsContainer(stepIndex: number): HTMLElement { |     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` |     // 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 |     // begin a log group | ||||||
|     beginLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) { |     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'}, |       const elJobLogGroupSummary = createElementFromAttrs('summary', {class: 'job-log-group-summary'}, | ||||||
|         this.createLogLine(stepIndex, startTime, { |         this.createLogLine(stepIndex, startTime, { | ||||||
|           index: line.index, |           index: line.index, | ||||||
| @@ -223,7 +238,7 @@ export default defineComponent({ | |||||||
|     }, |     }, | ||||||
|     // end a log group |     // end a log group | ||||||
|     endLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) { |     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._stepLogsActiveContainer = null; | ||||||
|       el.append(this.createLogLine(stepIndex, startTime, { |       el.append(this.createLogLine(stepIndex, startTime, { | ||||||
|         index: line.index, |         index: line.index, | ||||||
| @@ -393,7 +408,7 @@ export default defineComponent({ | |||||||
|       if (this.menuVisible) this.menuVisible = false; |       if (this.menuVisible) this.menuVisible = false; | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     toggleTimeDisplay(type: string) { |     toggleTimeDisplay(type: 'seconds' | 'stamp') { | ||||||
|       this.timeVisible[`log-time-${type}`] = !this.timeVisible[`log-time-${type}`]; |       this.timeVisible[`log-time-${type}`] = !this.timeVisible[`log-time-${type}`]; | ||||||
|       for (const el of (this.$refs.steps as HTMLElement).querySelectorAll(`.log-time-${type}`)) { |       for (const el of (this.$refs.steps as HTMLElement).querySelectorAll(`.log-time-${type}`)) { | ||||||
|         toggleElem(el, this.timeVisible[`log-time-${type}`]); |         toggleElem(el, this.timeVisible[`log-time-${type}`]); | ||||||
| @@ -422,9 +437,10 @@ export default defineComponent({ | |||||||
|       const selectedLogStep = window.location.hash; |       const selectedLogStep = window.location.hash; | ||||||
|       if (!selectedLogStep) return; |       if (!selectedLogStep) return; | ||||||
|       const [_, step, _line] = selectedLogStep.split('-'); |       const [_, step, _line] = selectedLogStep.split('-'); | ||||||
|       if (!this.currentJobStepsStates[step]) return; |       const stepNum = Number(step); | ||||||
|       if (!this.currentJobStepsStates[step].expanded && this.currentJobStepsStates[step].cursor === null) { |       if (!this.currentJobStepsStates[stepNum]) return; | ||||||
|         this.currentJobStepsStates[step].expanded = true; |       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 |         // need to await for load job if the step log is loaded for the first time | ||||||
|         // so logline can be selected by querySelector |         // so logline can be selected by querySelector | ||||||
|         await this.loadJob(); |         await this.loadJob(); | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | // @ts-expect-error - module exports no types | ||||||
| import {VueBarGraph} from 'vue-bar-graph'; | import {VueBarGraph} from 'vue-bar-graph'; | ||||||
| import {computed, onMounted, ref} from 'vue'; | import {computed, onMounted, ref} from 'vue'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -157,7 +157,7 @@ export default defineComponent({ | |||||||
|       // @ts-expect-error - el is unknown type |       // @ts-expect-error - el is unknown type | ||||||
|       return (el && el.length) ? el[0] : null; |       return (el && el.length) ? el[0] : null; | ||||||
|     }, |     }, | ||||||
|     keydown(e) { |     keydown(e: KeyboardEvent) { | ||||||
|       if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { |       if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { | ||||||
|         e.preventDefault(); |         e.preventDefault(); | ||||||
|  |  | ||||||
| @@ -181,7 +181,7 @@ export default defineComponent({ | |||||||
|         this.menuVisible = false; |         this.menuVisible = false; | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     handleTabSwitch(selectedTab) { |     handleTabSwitch(selectedTab: SelectedTab) { | ||||||
|       this.selectedTab = selectedTab; |       this.selectedTab = selectedTab; | ||||||
|       this.focusSearchField(); |       this.focusSearchField(); | ||||||
|       this.loadTabItems(); |       this.loadTabItems(); | ||||||
|   | |||||||
| @@ -80,10 +80,10 @@ export default defineComponent({ | |||||||
|     sortedContributors: {} as Record<string, any>, |     sortedContributors: {} as Record<string, any>, | ||||||
|     type: 'commits', |     type: 'commits', | ||||||
|     contributorsStats: {} as Record<string, any>, |     contributorsStats: {} as Record<string, any>, | ||||||
|     xAxisStart: null, |     xAxisStart: null as number | null, | ||||||
|     xAxisEnd: null, |     xAxisEnd: null as number | null, | ||||||
|     xAxisMin: null, |     xAxisMin: null as number | null, | ||||||
|     xAxisMax: null, |     xAxisMax: null as number | null, | ||||||
|   }), |   }), | ||||||
|   mounted() { |   mounted() { | ||||||
|     this.fetchGraphData(); |     this.fetchGraphData(); | ||||||
| @@ -99,7 +99,7 @@ export default defineComponent({ | |||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     sortContributors() { |     sortContributors() { | ||||||
|       const contributors = this.filterContributorWeeksByDateRange(); |       const contributors: Record<string, any> = this.filterContributorWeeksByDateRange(); | ||||||
|       const criteria = `total_${this.type}`; |       const criteria = `total_${this.type}`; | ||||||
|       this.sortedContributors = Object.values(contributors) |       this.sortedContributors = Object.values(contributors) | ||||||
|         .filter((contributor) => contributor[criteria] !== 0) |         .filter((contributor) => contributor[criteria] !== 0) | ||||||
| @@ -158,7 +158,7 @@ export default defineComponent({ | |||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     filterContributorWeeksByDateRange() { |     filterContributorWeeksByDateRange() { | ||||||
|       const filteredData = {}; |       const filteredData: Record<string, any> = {}; | ||||||
|       const data = this.contributorsStats; |       const data = this.contributorsStats; | ||||||
|       for (const key of Object.keys(data)) { |       for (const key of Object.keys(data)) { | ||||||
|         const user = data[key]; |         const user = data[key]; | ||||||
| @@ -196,7 +196,7 @@ export default defineComponent({ | |||||||
|       // Normally, chartjs handles this automatically, but it will resize the graph when you |       // 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. |       // zoom, pan etc. I think resizing the graph makes it harder to compare things visually. | ||||||
|       const maxValue = Math.max( |       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); |       const [coefficient, exp] = maxValue.toExponential().split('e').map(Number); | ||||||
|       if (coefficient % 1 === 0) return maxValue; |       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 |       // 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. |       // maxY value for each contributors' graph which again makes it harder to compare. | ||||||
|       const maxValue = Math.max( |       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); |       const [coefficient, exp] = maxValue.toExponential().split('e').map(Number); | ||||||
|       if (coefficient % 1 === 0) return maxValue; |       if (coefficient % 1 === 0) return maxValue; | ||||||
| @@ -232,8 +232,8 @@ export default defineComponent({ | |||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     updateOtherCharts({chart}: {chart: Chart}, reset: boolean = false) { |     updateOtherCharts({chart}: {chart: Chart}, reset: boolean = false) { | ||||||
|       const minVal = chart.options.scales.x.min; |       const minVal = Number(chart.options.scales.x.min); | ||||||
|       const maxVal = chart.options.scales.x.max; |       const maxVal = Number(chart.options.scales.x.max); | ||||||
|       if (reset) { |       if (reset) { | ||||||
|         this.xAxisMin = this.xAxisStart; |         this.xAxisMin = this.xAxisStart; | ||||||
|         this.xAxisMax = this.xAxisEnd; |         this.xAxisMax = this.xAxisEnd; | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ onUnmounted(() => { | |||||||
|   document.querySelector('#scoped-access-submit').removeEventListener('click', onClickSubmit); |   document.querySelector('#scoped-access-submit').removeEventListener('click', onClickSubmit); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| function onClickSubmit(e) { | function onClickSubmit(e: Event) { | ||||||
|   e.preventDefault(); |   e.preventDefault(); | ||||||
|  |  | ||||||
|   const warningEl = document.querySelector('#scoped-access-warning'); |   const warningEl = document.querySelector('#scoped-access-warning'); | ||||||
|   | |||||||
| @@ -90,7 +90,7 @@ export function initAdminCommon(): void { | |||||||
|     onOAuth2UseCustomURLChange(applyDefaultValues); |     onOAuth2UseCustomURLChange(applyDefaultValues); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   function onOAuth2UseCustomURLChange(applyDefaultValues) { |   function onOAuth2UseCustomURLChange(applyDefaultValues: boolean) { | ||||||
|     const provider = document.querySelector<HTMLInputElement>('#oauth2_provider').value; |     const provider = document.querySelector<HTMLInputElement>('#oauth2_provider').value; | ||||||
|     hideElem('.oauth2_use_custom_url_field'); |     hideElem('.oauth2_use_custom_url_field'); | ||||||
|     for (const input of document.querySelectorAll<HTMLInputElement>('.oauth2_use_custom_url_field input[required]')) { |     for (const input of document.querySelectorAll<HTMLInputElement>('.oauth2_use_custom_url_field input[required]')) { | ||||||
|   | |||||||
| @@ -5,9 +5,13 @@ const {pageData} = window.config; | |||||||
|  |  | ||||||
| async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) { | async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) { | ||||||
|   const [{Cite, plugins}] = await Promise.all([ |   const [{Cite, plugins}] = await Promise.all([ | ||||||
|  |     // @ts-expect-error: module exports no types | ||||||
|     import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'), |     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'), |     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'), |     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'), |     import(/* webpackChunkName: "citation-js-csl" */'@citation-js/plugin-csl'), | ||||||
|   ]); |   ]); | ||||||
|   const {citationFileContent} = pageData; |   const {citationFileContent} = pageData; | ||||||
|   | |||||||
| @@ -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"` |   // a '.show-panel' element can show a panel, by `data-panel="selector"` | ||||||
|   // if it has "toggle" class, it toggles the panel |   // if it has "toggle" class, it toggles the panel | ||||||
|   const el = e.currentTarget; |   const el = e.currentTarget as HTMLElement; | ||||||
|   e.preventDefault(); |   e.preventDefault(); | ||||||
|   const sel = el.getAttribute('data-panel'); |   const sel = el.getAttribute('data-panel'); | ||||||
|   if (el.classList.contains('toggle')) { |   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"` |   // 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(); |   e.preventDefault(); | ||||||
|   let sel = el.getAttribute('data-panel'); |   let sel = el.getAttribute('data-panel'); | ||||||
|   if (sel) { |   if (sel) { | ||||||
| @@ -98,13 +98,13 @@ function onHidePanelClick(e) { | |||||||
|   } |   } | ||||||
|   sel = el.getAttribute('data-panel-closest'); |   sel = el.getAttribute('data-panel-closest'); | ||||||
|   if (sel) { |   if (sel) { | ||||||
|     hideElem(el.parentNode.closest(sel)); |     hideElem((el.parentNode as HTMLElement).closest(sel)); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   throw new Error('no panel to hide'); // should never happen, otherwise there is a bug in code |   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. |   // 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. |   // Each "data-modal-{target}" attribute will be filled to target element's value or text-content. | ||||||
|   // * First, try to query '#target' |   // * First, try to query '#target' | ||||||
| @@ -112,7 +112,7 @@ function onShowModalClick(e) { | |||||||
|   // * Then, try to query '.target' |   // * Then, try to query '.target' | ||||||
|   // * Then, try to query 'target' as HTML tag |   // * 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. |   // 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(); |   e.preventDefault(); | ||||||
|   const modalSelector = el.getAttribute('data-modal'); |   const modalSelector = el.getAttribute('data-modal'); | ||||||
|   const elModal = document.querySelector(modalSelector); |   const elModal = document.querySelector(modalSelector); | ||||||
| @@ -137,9 +137,9 @@ function onShowModalClick(e) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (attrTargetAttr) { |     if (attrTargetAttr) { | ||||||
|       attrTarget[camelize(attrTargetAttr)] = attrib.value; |       (attrTarget as any)[camelize(attrTargetAttr)] = attrib.value; | ||||||
|     } else if (attrTarget.matches('input, textarea')) { |     } 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 { |     } else { | ||||||
|       attrTarget.textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p |       attrTarget.textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -75,7 +75,10 @@ async function formFetchAction(formEl: HTMLFormElement, e: SubmitEvent) { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   let reqUrl = formActionUrl; |   let reqUrl = formActionUrl; | ||||||
|   const reqOpt = {method: formMethod.toUpperCase(), body: null}; |   const reqOpt = { | ||||||
|  |     method: formMethod.toUpperCase(), | ||||||
|  |     body: null as FormData | null, | ||||||
|  |   }; | ||||||
|   if (formMethod.toLowerCase() === 'get') { |   if (formMethod.toLowerCase() === 'get') { | ||||||
|     const params = new URLSearchParams(); |     const params = new URLSearchParams(); | ||||||
|     for (const [key, value] of formData) { |     for (const [key, value] of formData) { | ||||||
|   | |||||||
| @@ -17,13 +17,13 @@ export function initGlobalEnterQuickSubmit() { | |||||||
|     if (e.key !== 'Enter') return; |     if (e.key !== 'Enter') return; | ||||||
|     const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey); |     const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey); | ||||||
|     if (hasCtrlOrMeta && e.target.matches('textarea')) { |     if (hasCtrlOrMeta && e.target.matches('textarea')) { | ||||||
|       if (handleGlobalEnterQuickSubmit(e.target)) { |       if (handleGlobalEnterQuickSubmit(e.target as HTMLElement)) { | ||||||
|         e.preventDefault(); |         e.preventDefault(); | ||||||
|       } |       } | ||||||
|     } else if (e.target.matches('input') && !e.target.closest('form')) { |     } 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 |       // 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 |       // eslint-disable-next-line unicorn/no-lonely-if | ||||||
|       if (handleGlobalEnterQuickSubmit(e.target)) { |       if (handleGlobalEnterQuickSubmit(e.target as HTMLElement)) { | ||||||
|         e.preventDefault(); |         e.preventDefault(); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -29,10 +29,10 @@ let elementIdCounter = 0; | |||||||
|  |  | ||||||
| /** | /** | ||||||
|  * validate if the given textarea is non-empty. |  * 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. |  * @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. |   // 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. |   // 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) { |   if (!textarea.value) { | ||||||
| @@ -49,16 +49,25 @@ export function validateTextareaNonEmpty(textarea) { | |||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type Heights = { | ||||||
|  |   minHeight?: string, | ||||||
|  |   height?: string, | ||||||
|  |   maxHeight?: string, | ||||||
|  | }; | ||||||
|  |  | ||||||
| type ComboMarkdownEditorOptions = { | type ComboMarkdownEditorOptions = { | ||||||
|   editorHeights?: {minHeight?: string, height?: string, maxHeight?: string}, |   editorHeights?: Heights, | ||||||
|   easyMDEOptions?: EasyMDE.Options, |   easyMDEOptions?: EasyMDE.Options, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | type ComboMarkdownEditorTextarea = HTMLTextAreaElement & {_giteaComboMarkdownEditor: any}; | ||||||
|  | type ComboMarkdownEditorContainer = HTMLElement & {_giteaComboMarkdownEditor?: any}; | ||||||
|  |  | ||||||
| export class ComboMarkdownEditor { | export class ComboMarkdownEditor { | ||||||
|   static EventEditorContentChanged = EventEditorContentChanged; |   static EventEditorContentChanged = EventEditorContentChanged; | ||||||
|   static EventUploadStateChanged = EventUploadStateChanged; |   static EventUploadStateChanged = EventUploadStateChanged; | ||||||
|  |  | ||||||
|   public container : HTMLElement; |   public container: HTMLElement; | ||||||
|  |  | ||||||
|   options: ComboMarkdownEditorOptions; |   options: ComboMarkdownEditorOptions; | ||||||
|  |  | ||||||
| @@ -70,7 +79,7 @@ export class ComboMarkdownEditor { | |||||||
|   easyMDEToolbarActions: any; |   easyMDEToolbarActions: any; | ||||||
|   easyMDEToolbarDefault: any; |   easyMDEToolbarDefault: any; | ||||||
|  |  | ||||||
|   textarea: HTMLTextAreaElement & {_giteaComboMarkdownEditor: any}; |   textarea: ComboMarkdownEditorTextarea; | ||||||
|   textareaMarkdownToolbar: HTMLElement; |   textareaMarkdownToolbar: HTMLElement; | ||||||
|   textareaAutosize: any; |   textareaAutosize: any; | ||||||
|  |  | ||||||
| @@ -81,7 +90,7 @@ export class ComboMarkdownEditor { | |||||||
|   previewUrl: string; |   previewUrl: string; | ||||||
|   previewContext: string; |   previewContext: string; | ||||||
|  |  | ||||||
|   constructor(container, options:ComboMarkdownEditorOptions = {}) { |   constructor(container: ComboMarkdownEditorContainer, options:ComboMarkdownEditorOptions = {}) { | ||||||
|     if (container._giteaComboMarkdownEditor) throw new Error('ComboMarkdownEditor already initialized'); |     if (container._giteaComboMarkdownEditor) throw new Error('ComboMarkdownEditor already initialized'); | ||||||
|     container._giteaComboMarkdownEditor = this; |     container._giteaComboMarkdownEditor = this; | ||||||
|     this.options = options; |     this.options = options; | ||||||
| @@ -98,7 +107,7 @@ export class ComboMarkdownEditor { | |||||||
|     await this.switchToUserPreference(); |     await this.switchToUserPreference(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   applyEditorHeights(el, heights) { |   applyEditorHeights(el: HTMLElement, heights: Heights) { | ||||||
|     if (!heights) return; |     if (!heights) return; | ||||||
|     if (heights.minHeight) el.style.minHeight = heights.minHeight; |     if (heights.minHeight) el.style.minHeight = heights.minHeight; | ||||||
|     if (heights.height) el.style.height = heights.height; |     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); |     this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(easyMde, this); | ||||||
|     const processed = []; |     const processed = []; | ||||||
|     for (const action of actions) { |     for (const action of actions) { | ||||||
| @@ -332,21 +341,21 @@ export class ComboMarkdownEditor { | |||||||
|     this.easyMDE = new EasyMDE(easyMDEOpt); |     this.easyMDE = new EasyMDE(easyMDEOpt); | ||||||
|     this.easyMDE.codemirror.on('change', () => triggerEditorContentChanged(this.container)); |     this.easyMDE.codemirror.on('change', () => triggerEditorContentChanged(this.container)); | ||||||
|     this.easyMDE.codemirror.setOption('extraKeys', { |     this.easyMDE.codemirror.setOption('extraKeys', { | ||||||
|       'Cmd-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()), |       'Cmd-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()), | ||||||
|       'Ctrl-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()), |       'Ctrl-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()), | ||||||
|       Enter: (cm) => { |       Enter: (cm: any) => { | ||||||
|         const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); |         const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); | ||||||
|         if (!tributeContainer || tributeContainer.style.display === 'none') { |         if (!tributeContainer || tributeContainer.style.display === 'none') { | ||||||
|           cm.execCommand('newlineAndIndent'); |           cm.execCommand('newlineAndIndent'); | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       Up: (cm) => { |       Up: (cm: any) => { | ||||||
|         const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); |         const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); | ||||||
|         if (!tributeContainer || tributeContainer.style.display === 'none') { |         if (!tributeContainer || tributeContainer.style.display === 'none') { | ||||||
|           return cm.execCommand('goLineUp'); |           return cm.execCommand('goLineUp'); | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       Down: (cm) => { |       Down: (cm: any) => { | ||||||
|         const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); |         const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); | ||||||
|         if (!tributeContainer || tributeContainer.style.display === 'none') { |         if (!tributeContainer || tributeContainer.style.display === 'none') { | ||||||
|           return cm.execCommand('goLineDown'); |           return cm.execCommand('goLineDown'); | ||||||
| @@ -354,14 +363,14 @@ export class ComboMarkdownEditor { | |||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|     this.applyEditorHeights(this.container.querySelector('.CodeMirror-scroll'), this.options.editorHeights); |     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) { |     if (this.dropzone) { | ||||||
|       initEasyMDEPaste(this.easyMDE, this.dropzone); |       initEasyMDEPaste(this.easyMDE, this.dropzone); | ||||||
|     } |     } | ||||||
|     hideElem(this.textareaMarkdownToolbar); |     hideElem(this.textareaMarkdownToolbar); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   value(v = undefined) { |   value(v: any = undefined) { | ||||||
|     if (v === undefined) { |     if (v === undefined) { | ||||||
|       if (this.easyMDE) { |       if (this.easyMDE) { | ||||||
|         return this.easyMDE.value(); |         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) return null; | ||||||
|   if (el.length) el = el[0]; |   if (el.length) el = el[0]; | ||||||
|   return el._giteaComboMarkdownEditor; |   return el._giteaComboMarkdownEditor; | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| export const EventEditorContentChanged = 'ce-editor-content-changed'; | export const EventEditorContentChanged = 'ce-editor-content-changed'; | ||||||
|  |  | ||||||
| export function triggerEditorContentChanged(target) { | export function triggerEditorContentChanged(target: HTMLElement) { | ||||||
|   target.dispatchEvent(new CustomEvent(EventEditorContentChanged, {bubbles: true})); |   target.dispatchEvent(new CustomEvent(EventEditorContentChanged, {bubbles: true})); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function textareaInsertText(textarea, value) { | export function textareaInsertText(textarea: HTMLTextAreaElement, value: string) { | ||||||
|   const startPos = textarea.selectionStart; |   const startPos = textarea.selectionStart; | ||||||
|   const endPos = textarea.selectionEnd; |   const endPos = textarea.selectionEnd; | ||||||
|   textarea.value = textarea.value.substring(0, startPos) + value + textarea.value.substring(endPos); |   textarea.value = textarea.value.substring(0, startPos) + value + textarea.value.substring(endPos); | ||||||
| @@ -20,7 +20,7 @@ type TextareaValueSelection = { | |||||||
|   selEnd: number; |   selEnd: number; | ||||||
| } | } | ||||||
|  |  | ||||||
| function handleIndentSelection(textarea: HTMLTextAreaElement, e) { | function handleIndentSelection(textarea: HTMLTextAreaElement, e: KeyboardEvent) { | ||||||
|   const selStart = textarea.selectionStart; |   const selStart = textarea.selectionStart; | ||||||
|   const selEnd = textarea.selectionEnd; |   const selEnd = textarea.selectionEnd; | ||||||
|   if (selEnd === selStart) return; // do not process when no selection |   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')); |   return Boolean(textarea.closest('text-expander')?.querySelector('.suggestions')); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function initTextareaMarkdown(textarea) { | export function initTextareaMarkdown(textarea: HTMLTextAreaElement) { | ||||||
|   textarea.addEventListener('keydown', (e) => { |   textarea.addEventListener('keydown', (e) => { | ||||||
|     if (isTextExpanderShown(textarea)) return; |     if (isTextExpanderShown(textarea)) return; | ||||||
|     if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) { |     if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) { | ||||||
|   | |||||||
| @@ -8,43 +8,46 @@ import { | |||||||
|   generateMarkdownLinkForAttachment, |   generateMarkdownLinkForAttachment, | ||||||
| } from '../dropzone.ts'; | } from '../dropzone.ts'; | ||||||
| import type CodeMirror from 'codemirror'; | import type CodeMirror from 'codemirror'; | ||||||
|  | import type EasyMDE from 'easymde'; | ||||||
|  | import type {DropzoneFile} from 'dropzone'; | ||||||
|  |  | ||||||
| let uploadIdCounter = 0; | let uploadIdCounter = 0; | ||||||
|  |  | ||||||
| export const EventUploadStateChanged = 'ce-upload-state-changed'; | export const EventUploadStateChanged = 'ce-upload-state-changed'; | ||||||
|  |  | ||||||
| export function triggerUploadStateChanged(target) { | export function triggerUploadStateChanged(target: HTMLElement) { | ||||||
|   target.dispatchEvent(new CustomEvent(EventUploadStateChanged, {bubbles: true})); |   target.dispatchEvent(new CustomEvent(EventUploadStateChanged, {bubbles: true})); | ||||||
| } | } | ||||||
|  |  | ||||||
| function uploadFile(dropzoneEl, file) { | function uploadFile(dropzoneEl: HTMLElement, file: File) { | ||||||
|   return new Promise((resolve) => { |   return new Promise((resolve) => { | ||||||
|     const curUploadId = uploadIdCounter++; |     const curUploadId = uploadIdCounter++; | ||||||
|     file._giteaUploadId = curUploadId; |     (file as any)._giteaUploadId = curUploadId; | ||||||
|     const dropzoneInst = dropzoneEl.dropzone; |     const dropzoneInst = dropzoneEl.dropzone; | ||||||
|     const onUploadDone = ({file}) => { |     const onUploadDone = ({file}: {file: any}) => { | ||||||
|       if (file._giteaUploadId === curUploadId) { |       if (file._giteaUploadId === curUploadId) { | ||||||
|         dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone); |         dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone); | ||||||
|         resolve(file); |         resolve(file); | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
|     dropzoneInst.on(DropzoneCustomEventUploadDone, onUploadDone); |     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 { | class TextareaEditor { | ||||||
|   editor : HTMLTextAreaElement; |   editor: HTMLTextAreaElement; | ||||||
|  |  | ||||||
|   constructor(editor) { |   constructor(editor: HTMLTextAreaElement) { | ||||||
|     this.editor = editor; |     this.editor = editor; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   insertPlaceholder(value) { |   insertPlaceholder(value: string) { | ||||||
|     textareaInsertText(this.editor, value); |     textareaInsertText(this.editor, value); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   replacePlaceholder(oldVal, newVal) { |   replacePlaceholder(oldVal: string, newVal: string) { | ||||||
|     const editor = this.editor; |     const editor = this.editor; | ||||||
|     const startPos = editor.selectionStart; |     const startPos = editor.selectionStart; | ||||||
|     const endPos = editor.selectionEnd; |     const endPos = editor.selectionEnd; | ||||||
| @@ -65,11 +68,11 @@ class TextareaEditor { | |||||||
| class CodeMirrorEditor { | class CodeMirrorEditor { | ||||||
|   editor: CodeMirror.EditorFromTextArea; |   editor: CodeMirror.EditorFromTextArea; | ||||||
|  |  | ||||||
|   constructor(editor) { |   constructor(editor: CodeMirror.EditorFromTextArea) { | ||||||
|     this.editor = editor; |     this.editor = editor; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   insertPlaceholder(value) { |   insertPlaceholder(value: string) { | ||||||
|     const editor = this.editor; |     const editor = this.editor; | ||||||
|     const startPoint = editor.getCursor('start'); |     const startPoint = editor.getCursor('start'); | ||||||
|     const endPoint = editor.getCursor('end'); |     const endPoint = editor.getCursor('end'); | ||||||
| @@ -80,7 +83,7 @@ class CodeMirrorEditor { | |||||||
|     triggerEditorContentChanged(editor.getTextArea()); |     triggerEditorContentChanged(editor.getTextArea()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   replacePlaceholder(oldVal, newVal) { |   replacePlaceholder(oldVal: string, newVal: string) { | ||||||
|     const editor = this.editor; |     const editor = this.editor; | ||||||
|     const endPoint = editor.getCursor('end'); |     const endPoint = editor.getCursor('end'); | ||||||
|     if (editor.getSelection() === oldVal) { |     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(); |   e.preventDefault(); | ||||||
|   for (const file of files) { |   for (const file of files) { | ||||||
|     const name = file.name.slice(0, file.name.lastIndexOf('.')); |     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(`!?\\[([^\\]]+)\\]\\(/?attachments/${fileUuid}\\)`, 'g'), ''); | ||||||
|   text = text.replace(new RegExp(`<img[^>]+src="/?attachments/${fileUuid}"[^>]*>`, 'g'), ''); |   text = text.replace(new RegExp(`<img[^>]+src="/?attachments/${fileUuid}"[^>]*>`, 'g'), ''); | ||||||
|   return text; |   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 |   // pasting with "shift" means "paste as original content" in most applications | ||||||
|   if (isShiftDown) return; // let the browser handle it |   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 | // extract text and images from "paste" event | ||||||
| function getPastedContent(e) { | function getPastedContent(e: ClipboardEvent) { | ||||||
|   const images = []; |   const images = []; | ||||||
|   for (const item of e.clipboardData?.items ?? []) { |   for (const item of e.clipboardData?.items ?? []) { | ||||||
|     if (item.type?.startsWith('image/')) { |     if (item.type?.startsWith('image/')) { | ||||||
| @@ -142,8 +145,8 @@ function getPastedContent(e) { | |||||||
|   return {text, images}; |   return {text, images}; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function initEasyMDEPaste(easyMDE, dropzoneEl) { | export function initEasyMDEPaste(easyMDE: EasyMDE, dropzoneEl: HTMLElement) { | ||||||
|   const editor = new CodeMirrorEditor(easyMDE.codemirror); |   const editor = new CodeMirrorEditor(easyMDE.codemirror as any); | ||||||
|   easyMDE.codemirror.on('paste', (_, e) => { |   easyMDE.codemirror.on('paste', (_, e) => { | ||||||
|     const {images} = getPastedContent(e); |     const {images} = getPastedContent(e); | ||||||
|     if (!images.length) return; |     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; |   let isShiftDown = false; | ||||||
|   textarea.addEventListener('keydown', (e) => { |   textarea.addEventListener('keydown', (e: KeyboardEvent) => { | ||||||
|     if (e.shiftKey) isShiftDown = true; |     if (e.shiftKey) isShiftDown = true; | ||||||
|   }); |   }); | ||||||
|   textarea.addEventListener('keyup', (e) => { |   textarea.addEventListener('keyup', (e: KeyboardEvent) => { | ||||||
|     if (!e.shiftKey) isShiftDown = false; |     if (!e.shiftKey) isShiftDown = false; | ||||||
|   }); |   }); | ||||||
|   textarea.addEventListener('paste', (e) => { |   textarea.addEventListener('paste', (e: ClipboardEvent) => { | ||||||
|     const {images, text} = getPastedContent(e); |     const {images, text} = getPastedContent(e); | ||||||
|     if (images.length && dropzoneEl) { |     if (images.length && dropzoneEl) { | ||||||
|       handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, images, e); |       handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, images, e); | ||||||
|     } else if (text) { |     } 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 (!e.dataTransfer.files.length) return; | ||||||
|     if (!dropzoneEl) return; |     if (!dropzoneEl) return; | ||||||
|     handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, e.dataTransfer.files, e); |     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); |     const newText = removeAttachmentLinksFromMarkdown(textarea.value, fileUuid); | ||||||
|     if (textarea.value !== newText) textarea.value = newText; |     if (textarea.value !== newText) textarea.value = newText; | ||||||
|   }); |   }); | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import {querySingleVisibleElem} from '../../utils/dom.ts'; | import {querySingleVisibleElem} from '../../utils/dom.ts'; | ||||||
|  |  | ||||||
| export function handleGlobalEnterQuickSubmit(target) { | export function handleGlobalEnterQuickSubmit(target: HTMLElement) { | ||||||
|   let form = target.closest('form'); |   let form = target.closest('form'); | ||||||
|   if (form) { |   if (form) { | ||||||
|     if (!form.checkValidity()) { |     if (!form.checkValidity()) { | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ export function initCompSearchUserBox() { | |||||||
|     minCharacters: 2, |     minCharacters: 2, | ||||||
|     apiSettings: { |     apiSettings: { | ||||||
|       url: `${appSubUrl}/user/search_candidates?q={query}`, |       url: `${appSubUrl}/user/search_candidates?q={query}`, | ||||||
|       onResponse(response) { |       onResponse(response: any) { | ||||||
|         const resultItems = []; |         const resultItems = []; | ||||||
|         const searchQuery = searchUserBox.querySelector('input').value; |         const searchQuery = searchUserBox.querySelector('input').value; | ||||||
|         const searchQueryUppercase = searchQuery.toUpperCase(); |         const searchQueryUppercase = searchQuery.toUpperCase(); | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import {parseIssueHref, parseRepoOwnerPathInfo} from '../../utils.ts'; | |||||||
| import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts'; | import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts'; | ||||||
| import {getIssueColor, getIssueIcon} from '../issue.ts'; | import {getIssueColor, getIssueIcon} from '../issue.ts'; | ||||||
| import {debounce} from 'perfect-debounce'; | 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 debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => { | ||||||
|   const issuePathInfo = parseIssueHref(window.location.href); |   const issuePathInfo = parseIssueHref(window.location.href); | ||||||
| @@ -32,8 +33,8 @@ const debouncedSuggestIssues = debounce((key: string, text: string) => new Promi | |||||||
|   resolve({matched: true, fragment: ul}); |   resolve({matched: true, fragment: ul}); | ||||||
| }), 100); | }), 100); | ||||||
|  |  | ||||||
| export function initTextExpander(expander) { | export function initTextExpander(expander: TextExpanderElement) { | ||||||
|   expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => { |   expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}: Record<string, any>) => { | ||||||
|     if (key === ':') { |     if (key === ':') { | ||||||
|       const matches = matchEmoji(text); |       const matches = matchEmoji(text); | ||||||
|       if (!matches.length) return provide({matched: false}); |       if (!matches.length) return provide({matched: false}); | ||||||
| @@ -84,7 +85,7 @@ export function initTextExpander(expander) { | |||||||
|       provide(debouncedSuggestIssues(key, text)); |       provide(debouncedSuggestIssues(key, text)); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|   expander?.addEventListener('text-expander-value', ({detail}) => { |   expander?.addEventListener('text-expander-value', ({detail}: Record<string, any>) => { | ||||||
|     if (detail?.item) { |     if (detail?.item) { | ||||||
|       // add a space after @mentions and #issue as it's likely the user wants one |       // add a space after @mentions and #issue as it's likely the user wants one | ||||||
|       const suffix = ['@', '#'].includes(detail.key) ? ' ' : ''; |       const suffix = ['@', '#'].includes(detail.key) ? ' ' : ''; | ||||||
|   | |||||||
| @@ -4,11 +4,11 @@ import {parseIssueHref} from '../utils.ts'; | |||||||
| import {createTippy} from '../modules/tippy.ts'; | import {createTippy} from '../modules/tippy.ts'; | ||||||
|  |  | ||||||
| export function initContextPopups() { | export function initContextPopups() { | ||||||
|   const refIssues = document.querySelectorAll('.ref-issue'); |   const refIssues = document.querySelectorAll<HTMLElement>('.ref-issue'); | ||||||
|   attachRefIssueContextPopup(refIssues); |   attachRefIssueContextPopup(refIssues); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function attachRefIssueContextPopup(refIssues) { | export function attachRefIssueContextPopup(refIssues: NodeListOf<HTMLElement>) { | ||||||
|   for (const refIssue of refIssues) { |   for (const refIssue of refIssues) { | ||||||
|     if (refIssue.classList.contains('ref-external-issue')) continue; |     if (refIssue.classList.contains('ref-external-issue')) continue; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -46,7 +46,7 @@ export function initCopyContent() { | |||||||
|       showTemporaryTooltip(btn, i18n.copy_success); |       showTemporaryTooltip(btn, i18n.copy_success); | ||||||
|     } else { |     } else { | ||||||
|       if (isRasterImage) { |       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); |         showTemporaryTooltip(btn, success ? i18n.copy_success : i18n.copy_error); | ||||||
|       } else { |       } else { | ||||||
|         showTemporaryTooltip(btn, i18n.copy_error); |         showTemporaryTooltip(btn, i18n.copy_error); | ||||||
|   | |||||||
| @@ -6,16 +6,18 @@ import {GET, POST} from '../modules/fetch.ts'; | |||||||
| import {showErrorToast} from '../modules/toast.ts'; | import {showErrorToast} from '../modules/toast.ts'; | ||||||
| import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts'; | import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts'; | ||||||
| import {isImageFile, isVideoFile} from '../utils.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; | const {csrfToken, i18n} = window.config; | ||||||
|  |  | ||||||
|  | type CustomDropzoneFile = DropzoneFile & {uuid: string}; | ||||||
|  |  | ||||||
| // dropzone has its owner event dispatcher (emitter) | // dropzone has its owner event dispatcher (emitter) | ||||||
| export const DropzoneCustomEventReloadFiles = 'dropzone-custom-reload-files'; | export const DropzoneCustomEventReloadFiles = 'dropzone-custom-reload-files'; | ||||||
| export const DropzoneCustomEventRemovedFile = 'dropzone-custom-removed-file'; | export const DropzoneCustomEventRemovedFile = 'dropzone-custom-removed-file'; | ||||||
| export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done'; | 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([ |   const [{default: Dropzone}] = await Promise.all([ | ||||||
|     import(/* webpackChunkName: "dropzone" */'dropzone'), |     import(/* webpackChunkName: "dropzone" */'dropzone'), | ||||||
|     import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'), |     import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'), | ||||||
| @@ -23,7 +25,7 @@ async function createDropzone(el, opts) { | |||||||
|   return new Dropzone(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})`; |   let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`; | ||||||
|   if (isImageFile(file)) { |   if (isImageFile(file)) { | ||||||
|     fileMarkdown = `!${fileMarkdown}`; |     fileMarkdown = `!${fileMarkdown}`; | ||||||
| @@ -43,7 +45,7 @@ export function generateMarkdownLinkForAttachment(file, {width, dppx}: {width?: | |||||||
|   return fileMarkdown; |   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 |   // 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 |   // The "<a>" element has a hardcoded cursor: pointer because the default is overridden by .dropzone | ||||||
|   const copyLinkEl = createElementFromHTML(` |   const copyLinkEl = createElementFromHTML(` | ||||||
| @@ -58,6 +60,8 @@ function addCopyLink(file) { | |||||||
|   file.previewTemplate.append(copyLinkEl); |   file.previewTemplate.append(copyLinkEl); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type FileUuidDict = Record<string, {submitted: boolean}>; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * @param {HTMLElement} dropzoneEl |  * @param {HTMLElement} dropzoneEl | ||||||
|  */ |  */ | ||||||
| @@ -67,7 +71,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) { | |||||||
|   const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url'); |   const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url'); | ||||||
|  |  | ||||||
|   let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event |   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> = { |   const opts: Record<string, any> = { | ||||||
|     url: dropzoneEl.getAttribute('data-upload-url'), |     url: dropzoneEl.getAttribute('data-upload-url'), | ||||||
|     headers: {'X-Csrf-Token': csrfToken}, |     headers: {'X-Csrf-Token': csrfToken}, | ||||||
| @@ -89,7 +93,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) { | |||||||
|   // "http://localhost:3000/owner/repo/issues/[object%20Event]" |   // "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]">' |   // 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); |   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; |     file.uuid = resp.uuid; | ||||||
|     fileUuidDict[file.uuid] = {submitted: false}; |     fileUuidDict[file.uuid] = {submitted: false}; | ||||||
|     const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${resp.uuid}`, value: resp.uuid}); |     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.emit(DropzoneCustomEventUploadDone, {file}); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   dzInst.on('removedfile', async (file: DropzoneFile & {uuid: string}) => { |   dzInst.on('removedfile', async (file: CustomDropzoneFile) => { | ||||||
|     if (disableRemovedfileEvent) return; |     if (disableRemovedfileEvent) return; | ||||||
|  |  | ||||||
|     dzInst.emit(DropzoneCustomEventRemovedFile, {fileUuid: file.uuid}); |     dzInst.emit(DropzoneCustomEventRemovedFile, {fileUuid: file.uuid}); | ||||||
|   | |||||||
| @@ -15,13 +15,13 @@ export const emojiKeys = Object.keys(tempMap).sort((a, b) => { | |||||||
|   return a.localeCompare(b); |   return a.localeCompare(b); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| const emojiMap = {}; | const emojiMap: Record<string, string> = {}; | ||||||
| for (const key of emojiKeys) { | for (const key of emojiKeys) { | ||||||
|   emojiMap[key] = tempMap[key]; |   emojiMap[key] = tempMap[key]; | ||||||
| } | } | ||||||
|  |  | ||||||
| // retrieve HTML for given emoji name | // retrieve HTML for given emoji name | ||||||
| export function emojiHTML(name) { | export function emojiHTML(name: string) { | ||||||
|   let inner; |   let inner; | ||||||
|   if (Object.hasOwn(customEmojis, name)) { |   if (Object.hasOwn(customEmojis, name)) { | ||||||
|     inner = `<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`; |     inner = `<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`; | ||||||
| @@ -33,6 +33,6 @@ export function emojiHTML(name) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // retrieve string for given emoji name | // retrieve string for given emoji name | ||||||
| export function emojiString(name) { | export function emojiString(name: string) { | ||||||
|   return emojiMap[name] || `:${name}:`; |   return emojiMap[name] || `:${name}:`; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 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. | // 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); |   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) { |   if (newFold && fileContentBox.getBoundingClientRect().top < 0) { | ||||||
|     fileContentBox.scrollIntoView(); |     fileContentBox.scrollIntoView(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| // Like `setFileFolding`, except that it automatically inverts the current file folding state. | // 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'); |   setFileFolding(fileContentBox, foldArrow, fileContentBox.getAttribute('data-folded') !== 'true'); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ export function initHeatmap() { | |||||||
|   if (!el) return; |   if (!el) return; | ||||||
|  |  | ||||||
|   try { |   try { | ||||||
|     const heatmap = {}; |     const heatmap: Record<string, number> = {}; | ||||||
|     for (const {contributions, timestamp} of JSON.parse(el.getAttribute('data-heatmap-data'))) { |     for (const {contributions, timestamp} of JSON.parse(el.getAttribute('data-heatmap-data'))) { | ||||||
|       // Convert to user timezone and sum contributions by date |       // Convert to user timezone and sum contributions by date | ||||||
|       const dateStr = new Date(timestamp * 1000).toDateString(); |       const dateStr = new Date(timestamp * 1000).toDateString(); | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import {hideElem, loadElem, queryElemChildren, queryElems} from '../utils/dom.ts | |||||||
| import {parseDom} from '../utils.ts'; | import {parseDom} from '../utils.ts'; | ||||||
| import {fomanticQuery} from '../modules/fomantic/base.ts'; | import {fomanticQuery} from '../modules/fomantic/base.ts'; | ||||||
|  |  | ||||||
| function getDefaultSvgBoundsIfUndefined(text, src) { | function getDefaultSvgBoundsIfUndefined(text: string, src: string) { | ||||||
|   const defaultSize = 300; |   const defaultSize = 300; | ||||||
|   const maxSize = 99999; |   const maxSize = 99999; | ||||||
|  |  | ||||||
| @@ -38,7 +38,7 @@ function getDefaultSvgBoundsIfUndefined(text, src) { | |||||||
|   return null; |   return null; | ||||||
| } | } | ||||||
|  |  | ||||||
| function createContext(imageAfter, imageBefore) { | function createContext(imageAfter: HTMLImageElement, imageBefore: HTMLImageElement) { | ||||||
|   const sizeAfter = { |   const sizeAfter = { | ||||||
|     width: imageAfter?.width || 0, |     width: imageAfter?.width || 0, | ||||||
|     height: imageAfter?.height || 0, |     height: imageAfter?.height || 0, | ||||||
| @@ -123,7 +123,7 @@ class ImageDiff { | |||||||
|     queryElemChildren(containerEl, '.image-diff-tabs', (el) => el.classList.remove('is-loading')); |     queryElemChildren(containerEl, '.image-diff-tabs', (el) => el.classList.remove('is-loading')); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   initSideBySide(sizes) { |   initSideBySide(sizes: Record<string, any>) { | ||||||
|     let factor = 1; |     let factor = 1; | ||||||
|     if (sizes.maxSize.width > (this.diffContainerWidth - 24) / 2) { |     if (sizes.maxSize.width > (this.diffContainerWidth - 24) / 2) { | ||||||
|       factor = (this.diffContainerWidth - 24) / 2 / sizes.maxSize.width; |       factor = (this.diffContainerWidth - 24) / 2 / sizes.maxSize.width; | ||||||
| @@ -176,7 +176,7 @@ class ImageDiff { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   initSwipe(sizes) { |   initSwipe(sizes: Record<string, any>) { | ||||||
|     let factor = 1; |     let factor = 1; | ||||||
|     if (sizes.maxSize.width > this.diffContainerWidth - 12) { |     if (sizes.maxSize.width > this.diffContainerWidth - 12) { | ||||||
|       factor = (this.diffContainerWidth - 12) / sizes.maxSize.width; |       factor = (this.diffContainerWidth - 12) / sizes.maxSize.width; | ||||||
| @@ -215,14 +215,14 @@ class ImageDiff { | |||||||
|  |  | ||||||
|     this.containerEl.querySelector('.swipe-bar').addEventListener('mousedown', (e) => { |     this.containerEl.querySelector('.swipe-bar').addEventListener('mousedown', (e) => { | ||||||
|       e.preventDefault(); |       e.preventDefault(); | ||||||
|       this.initSwipeEventListeners(e.currentTarget); |       this.initSwipeEventListeners(e.currentTarget as HTMLElement); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   initSwipeEventListeners(swipeBar) { |   initSwipeEventListeners(swipeBar: HTMLElement) { | ||||||
|     const swipeFrame = swipeBar.parentNode; |     const swipeFrame = swipeBar.parentNode as HTMLElement; | ||||||
|     const width = swipeFrame.clientWidth; |     const width = swipeFrame.clientWidth; | ||||||
|     const onSwipeMouseMove = (e) => { |     const onSwipeMouseMove = (e: MouseEvent) => { | ||||||
|       e.preventDefault(); |       e.preventDefault(); | ||||||
|       const rect = swipeFrame.getBoundingClientRect(); |       const rect = swipeFrame.getBoundingClientRect(); | ||||||
|       const value = Math.max(0, Math.min(e.clientX - rect.left, width)); |       const value = Math.max(0, Math.min(e.clientX - rect.left, width)); | ||||||
| @@ -237,7 +237,7 @@ class ImageDiff { | |||||||
|     document.addEventListener('mouseup', removeEventListeners); |     document.addEventListener('mouseup', removeEventListeners); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   initOverlay(sizes) { |   initOverlay(sizes: Record<string, any>) { | ||||||
|     let factor = 1; |     let factor = 1; | ||||||
|     if (sizes.maxSize.width > this.diffContainerWidth - 12) { |     if (sizes.maxSize.width > this.diffContainerWidth - 12) { | ||||||
|       factor = (this.diffContainerWidth - 12) / sizes.maxSize.width; |       factor = (this.diffContainerWidth - 12) / sizes.maxSize.width; | ||||||
|   | |||||||
| @@ -12,11 +12,12 @@ export function initInstall() { | |||||||
|     initPreInstall(); |     initPreInstall(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| function initPreInstall() { | function initPreInstall() { | ||||||
|   const defaultDbUser = 'gitea'; |   const defaultDbUser = 'gitea'; | ||||||
|   const defaultDbName = 'gitea'; |   const defaultDbName = 'gitea'; | ||||||
|  |  | ||||||
|   const defaultDbHosts = { |   const defaultDbHosts: Record<string, string> = { | ||||||
|     mysql: '127.0.0.1:3306', |     mysql: '127.0.0.1:3306', | ||||||
|     postgres: '127.0.0.1:5432', |     postgres: '127.0.0.1:5432', | ||||||
|     mssql: '127.0.0.1:1433', |     mssql: '127.0.0.1:1433', | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ function initOrgTeamSearchRepoBox() { | |||||||
|     minCharacters: 2, |     minCharacters: 2, | ||||||
|     apiSettings: { |     apiSettings: { | ||||||
|       url: `${appSubUrl}/repo/search?q={query}&uid=${$searchRepoBox.data('uid')}`, |       url: `${appSubUrl}/repo/search?q={query}&uid=${$searchRepoBox.data('uid')}`, | ||||||
|       onResponse(response) { |       onResponse(response: any) { | ||||||
|         const items = []; |         const items = []; | ||||||
|         for (const item of response.data) { |         for (const item of response.data) { | ||||||
|           items.push({ |           items.push({ | ||||||
|   | |||||||
| @@ -59,13 +59,13 @@ export function initViewedCheckboxListenerFor() { | |||||||
|       const fileName = checkbox.getAttribute('name'); |       const fileName = checkbox.getAttribute('name'); | ||||||
|  |  | ||||||
|       // check if the file is in our difftreestore and if we find it -> change the IsViewed status |       // 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) { |       if (fileInPageData) { | ||||||
|         fileInPageData.IsViewed = this.checked; |         fileInPageData.IsViewed = this.checked; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // Unfortunately, actual forms cause too many problems, hence another approach is needed |       // Unfortunately, actual forms cause too many problems, hence another approach is needed | ||||||
|       const files = {}; |       const files: Record<string, boolean> = {}; | ||||||
|       files[fileName] = this.checked; |       files[fileName] = this.checked; | ||||||
|       const data: Record<string, any> = {files}; |       const data: Record<string, any> = {files}; | ||||||
|       const headCommitSHA = form.getAttribute('data-headcommit'); |       const headCommitSHA = form.getAttribute('data-headcommit'); | ||||||
| @@ -82,13 +82,13 @@ export function initViewedCheckboxListenerFor() { | |||||||
| export function initExpandAndCollapseFilesButton() { | export function initExpandAndCollapseFilesButton() { | ||||||
|   // expand btn |   // expand btn | ||||||
|   document.querySelector(expandFilesBtnSelector)?.addEventListener('click', () => { |   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); |       setFileFolding(box, box.querySelector('.fold-file'), false); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|   // collapse btn, need to exclude the div of “show more” |   // collapse btn, need to exclude the div of “show more” | ||||||
|   document.querySelector(collapseFilesBtnSelector)?.addEventListener('click', () => { |   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; |       if (box.getAttribute('id') === 'diff-incomplete') continue; | ||||||
|       setFileFolding(box, box.querySelector('.fold-file'), true); |       setFileFolding(box, box.querySelector('.fold-file'), true); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -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 {POST} from '../modules/fetch.ts'; | ||||||
| import {showErrorToast} from '../modules/toast.ts'; | import {showErrorToast} from '../modules/toast.ts'; | ||||||
| import {sleep} from '../utils.ts'; | import {sleep} from '../utils.ts'; | ||||||
| @@ -7,10 +7,10 @@ import {createApp} from 'vue'; | |||||||
| import {toOriginUrl} from '../utils/url.ts'; | import {toOriginUrl} from '../utils/url.ts'; | ||||||
| import {createTippy} from '../modules/tippy.ts'; | import {createTippy} from '../modules/tippy.ts'; | ||||||
|  |  | ||||||
| async function onDownloadArchive(e) { | async function onDownloadArchive(e: DOMEvent<MouseEvent>) { | ||||||
|   e.preventDefault(); |   e.preventDefault(); | ||||||
|   // there are many places using the "archive-link", eg: the dropdown on the repo code page, the release list |   // 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; |   const targetLoading = el.closest('.ui.dropdown') ?? el; | ||||||
|   targetLoading.classList.add('is-loading', 'loading-icon-2px'); |   targetLoading.classList.add('is-loading', 'loading-icon-2px'); | ||||||
|   try { |   try { | ||||||
| @@ -107,7 +107,7 @@ export function initRepoCloneButtons() { | |||||||
|   queryElems(document, '.clone-buttons-combo', initCloneSchemeUrlSelection); |   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 { |   try { | ||||||
|     const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})}); |     const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})}); | ||||||
|     if (!response.ok) { |     if (!response.ok) { | ||||||
|   | |||||||
| @@ -168,7 +168,7 @@ function onShowMoreFiles() { | |||||||
|   initDiffHeaderPopup(); |   initDiffHeaderPopup(); | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function loadMoreFiles(url) { | export async function loadMoreFiles(url: string) { | ||||||
|   const target = document.querySelector('a#diff-show-more-files'); |   const target = document.querySelector('a#diff-show-more-files'); | ||||||
|   if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) { |   if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) { | ||||||
|     return; |     return; | ||||||
|   | |||||||
| @@ -168,7 +168,7 @@ export function initRepoEditor() { | |||||||
|       silent: true, |       silent: true, | ||||||
|       dirtyClass: dirtyFileClass, |       dirtyClass: dirtyFileClass, | ||||||
|       fieldSelector: ':input:not(.commit-form-wrapper :input)', |       fieldSelector: ':input:not(.commit-form-wrapper :input)', | ||||||
|       change($form) { |       change($form: any) { | ||||||
|         const dirty = $form[0]?.classList.contains(dirtyFileClass); |         const dirty = $form[0]?.classList.contains(dirtyFileClass); | ||||||
|         commitButton.disabled = !dirty; |         commitButton.disabled = !dirty; | ||||||
|       }, |       }, | ||||||
|   | |||||||
| @@ -4,13 +4,15 @@ import {pathEscapeSegments} from '../utils/url.ts'; | |||||||
| import {GET} from '../modules/fetch.ts'; | import {GET} from '../modules/fetch.ts'; | ||||||
|  |  | ||||||
| const threshold = 50; | const threshold = 50; | ||||||
| let files = []; | let files: Array<string> = []; | ||||||
| let repoFindFileInput, repoFindFileTableBody, repoFindFileNoResult; | let repoFindFileInput: HTMLInputElement; | ||||||
|  | let repoFindFileTableBody: HTMLElement; | ||||||
|  | let repoFindFileNoResult: HTMLElement; | ||||||
|  |  | ||||||
| // return the case-insensitive sub-match result as an array:  [unmatched, matched, unmatched, matched, ...] | // 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 | // res[even] is unmatched, res[odd] is matched, see unit tests for examples | ||||||
| // argument subLower must be a lower-cased string. | // argument subLower must be a lower-cased string. | ||||||
| export function strSubMatch(full, subLower) { | export function strSubMatch(full: string, subLower: string) { | ||||||
|   const res = ['']; |   const res = ['']; | ||||||
|   let i = 0, j = 0; |   let i = 0, j = 0; | ||||||
|   const fullLower = full.toLowerCase(); |   const fullLower = full.toLowerCase(); | ||||||
| @@ -38,7 +40,7 @@ export function strSubMatch(full, subLower) { | |||||||
|   return res; |   return res; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function calcMatchedWeight(matchResult) { | export function calcMatchedWeight(matchResult: Array<any>) { | ||||||
|   let weight = 0; |   let weight = 0; | ||||||
|   for (let i = 0; i < matchResult.length; i++) { |   for (let i = 0; i < matchResult.length; i++) { | ||||||
|     if (i % 2 === 1) { // matches are on odd indices, see strSubMatch |     if (i % 2 === 1) { // matches are on odd indices, see strSubMatch | ||||||
| @@ -49,7 +51,7 @@ export function calcMatchedWeight(matchResult) { | |||||||
|   return weight; |   return weight; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function filterRepoFilesWeighted(files, filter) { | export function filterRepoFilesWeighted(files: Array<string>, filter: string) { | ||||||
|   let filterResult = []; |   let filterResult = []; | ||||||
|   if (filter) { |   if (filter) { | ||||||
|     const filterLower = filter.toLowerCase(); |     const filterLower = filter.toLowerCase(); | ||||||
| @@ -71,7 +73,7 @@ export function filterRepoFilesWeighted(files, filter) { | |||||||
|   return filterResult; |   return filterResult; | ||||||
| } | } | ||||||
|  |  | ||||||
| function filterRepoFiles(filter) { | function filterRepoFiles(filter: string) { | ||||||
|   const treeLink = repoFindFileInput.getAttribute('data-url-tree-link'); |   const treeLink = repoFindFileInput.getAttribute('data-url-tree-link'); | ||||||
|   repoFindFileTableBody.innerHTML = ''; |   repoFindFileTableBody.innerHTML = ''; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -92,7 +92,7 @@ export function initRepoTopicBar() { | |||||||
|       onResponse(this: any, res: any) { |       onResponse(this: any, res: any) { | ||||||
|         const formattedResponse = { |         const formattedResponse = { | ||||||
|           success: false, |           success: false, | ||||||
|           results: [], |           results: [] as Array<Record<string, any>>, | ||||||
|         }; |         }; | ||||||
|         const query = stripTags(this.urlData.query.trim()); |         const query = stripTags(this.urlData.query.trim()); | ||||||
|         let found_query = false; |         let found_query = false; | ||||||
| @@ -134,12 +134,12 @@ export function initRepoTopicBar() { | |||||||
|         return formattedResponse; |         return formattedResponse; | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     onLabelCreate(value) { |     onLabelCreate(value: string) { | ||||||
|       value = value.toLowerCase().trim(); |       value = value.toLowerCase().trim(); | ||||||
|       this.attr('data-value', value).contents().first().replaceWith(value); |       this.attr('data-value', value).contents().first().replaceWith(value); | ||||||
|       return fomanticQuery(this); |       return fomanticQuery(this); | ||||||
|     }, |     }, | ||||||
|     onAdd(addedValue, _addedText, $addedChoice) { |     onAdd(addedValue: string, _addedText: any, $addedChoice: any) { | ||||||
|       addedValue = addedValue.toLowerCase().trim(); |       addedValue = addedValue.toLowerCase().trim(); | ||||||
|       $addedChoice[0].setAttribute('data-value', addedValue); |       $addedChoice[0].setAttribute('data-value', addedValue); | ||||||
|       $addedChoice[0].setAttribute('data-text', addedValue); |       $addedChoice[0].setAttribute('data-text', addedValue); | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo | |||||||
|   $fomanticDropdownOptions.dropdown({ |   $fomanticDropdownOptions.dropdown({ | ||||||
|     showOnFocus: false, |     showOnFocus: false, | ||||||
|     allowReselection: true, |     allowReselection: true, | ||||||
|     async onChange(_value, _text, $item) { |     async onChange(_value: string, _text: string, $item: any) { | ||||||
|       const optionItem = $item.data('option-item'); |       const optionItem = $item.data('option-item'); | ||||||
|       if (optionItem === 'delete') { |       if (optionItem === 'delete') { | ||||||
|         if (window.confirm(i18nTextDeleteFromHistoryConfirm)) { |         if (window.confirm(i18nTextDeleteFromHistoryConfirm)) { | ||||||
| @@ -115,7 +115,7 @@ function showContentHistoryMenu(issueBaseUrl: string, elCommentItem: Element, co | |||||||
|     onHide() { |     onHide() { | ||||||
|       $fomanticDropdown.dropdown('change values', null); |       $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) { |       if (value && !$item.find('[data-history-is-deleted=1]').length) { | ||||||
|         showContentHistoryDetail(issueBaseUrl, commentId, value, itemHtml); |         showContentHistoryDetail(issueBaseUrl, commentId, value, itemHtml); | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -2,14 +2,14 @@ import {handleReply} from './repo-issue.ts'; | |||||||
| import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; | import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; | ||||||
| import {POST} from '../modules/fetch.ts'; | import {POST} from '../modules/fetch.ts'; | ||||||
| import {showErrorToast} from '../modules/toast.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 {attachRefIssueContextPopup} from './contextpopup.ts'; | ||||||
| import {initCommentContent, initMarkupContent} from '../markup/content.ts'; | import {initCommentContent, initMarkupContent} from '../markup/content.ts'; | ||||||
| import {triggerUploadStateChanged} from './comp/EditorUpload.ts'; | import {triggerUploadStateChanged} from './comp/EditorUpload.ts'; | ||||||
| import {convertHtmlToMarkdown} from '../markup/html2markdown.ts'; | import {convertHtmlToMarkdown} from '../markup/html2markdown.ts'; | ||||||
| import {applyAreYouSure, reinitializeAreYouSure} from '../vendor/jquery.are-you-sure.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'); |   const clickTarget = e.target.closest('.edit-content'); | ||||||
|   if (!clickTarget) return; |   if (!clickTarget) return; | ||||||
|  |  | ||||||
| @@ -21,14 +21,14 @@ async function tryOnEditContent(e) { | |||||||
|  |  | ||||||
|   let comboMarkdownEditor : ComboMarkdownEditor; |   let comboMarkdownEditor : ComboMarkdownEditor; | ||||||
|  |  | ||||||
|   const cancelAndReset = (e) => { |   const cancelAndReset = (e: Event) => { | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|     showElem(renderContent); |     showElem(renderContent); | ||||||
|     hideElem(editContentZone); |     hideElem(editContentZone); | ||||||
|     comboMarkdownEditor.dropzoneReloadFiles(); |     comboMarkdownEditor.dropzoneReloadFiles(); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const saveAndRefresh = async (e) => { |   const saveAndRefresh = async (e: Event) => { | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|     // we are already in a form, do not bubble up to the document otherwise there will be other "form submit handlers" |     // 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) |     // 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 { |       } else { | ||||||
|         renderContent.innerHTML = data.content; |         renderContent.innerHTML = data.content; | ||||||
|         rawContent.textContent = comboMarkdownEditor.value(); |         rawContent.textContent = comboMarkdownEditor.value(); | ||||||
|         const refIssues = renderContent.querySelectorAll('p .ref-issue'); |         const refIssues = renderContent.querySelectorAll<HTMLElement>('p .ref-issue'); | ||||||
|         attachRefIssueContextPopup(refIssues); |         attachRefIssueContextPopup(refIssues); | ||||||
|       } |       } | ||||||
|       const content = segment; |       const content = segment; | ||||||
| @@ -125,7 +125,7 @@ function extractSelectedMarkdown(container: HTMLElement) { | |||||||
|   return convertHtmlToMarkdown(el); |   return convertHtmlToMarkdown(el); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function tryOnQuoteReply(e) { | async function tryOnQuoteReply(e: Event) { | ||||||
|   const clickTarget = (e.target as HTMLElement).closest('.quote-reply'); |   const clickTarget = (e.target as HTMLElement).closest('.quote-reply'); | ||||||
|   if (!clickTarget) return; |   if (!clickTarget) return; | ||||||
|  |  | ||||||
| @@ -139,7 +139,7 @@ async function tryOnQuoteReply(e) { | |||||||
|  |  | ||||||
|   let editor; |   let editor; | ||||||
|   if (clickTarget.classList.contains('quote-reply-diff')) { |   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); |     editor = await handleReply(replyBtn); | ||||||
|   } else { |   } else { | ||||||
|     // for normal issue/comment page |     // for normal issue/comment page | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import {createSortable} from '../modules/sortable.ts'; | |||||||
| import {DELETE, POST} from '../modules/fetch.ts'; | import {DELETE, POST} from '../modules/fetch.ts'; | ||||||
| import {parseDom} from '../utils.ts'; | import {parseDom} from '../utils.ts'; | ||||||
| import {fomanticQuery} from '../modules/fomantic/base.ts'; | import {fomanticQuery} from '../modules/fomantic/base.ts'; | ||||||
|  | import type {SortableEvent} from 'sortablejs'; | ||||||
|  |  | ||||||
| function initRepoIssueListCheckboxes() { | function initRepoIssueListCheckboxes() { | ||||||
|   const issueSelectAll = document.querySelector<HTMLInputElement>('.issue-checkbox-all'); |   const issueSelectAll = document.querySelector<HTMLInputElement>('.issue-checkbox-all'); | ||||||
| @@ -104,7 +105,7 @@ function initDropdownUserRemoteSearch(el: Element) { | |||||||
|   $searchDropdown.dropdown('setting', { |   $searchDropdown.dropdown('setting', { | ||||||
|     fullTextSearch: true, |     fullTextSearch: true, | ||||||
|     selectOnKeydown: false, |     selectOnKeydown: false, | ||||||
|     action: (_text, value) => { |     action: (_text: string, value: string) => { | ||||||
|       window.location.href = actionJumpUrl.replace('{username}', encodeURIComponent(value)); |       window.location.href = actionJumpUrl.replace('{username}', encodeURIComponent(value)); | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
| @@ -133,7 +134,7 @@ function initDropdownUserRemoteSearch(el: Element) { | |||||||
|     $searchDropdown.dropdown('setting', 'apiSettings', { |     $searchDropdown.dropdown('setting', 'apiSettings', { | ||||||
|       cache: false, |       cache: false, | ||||||
|       url: `${searchUrl}&q={query}`, |       url: `${searchUrl}&q={query}`, | ||||||
|       onResponse(resp) { |       onResponse(resp: any) { | ||||||
|         // the content is provided by backend IssuePosters handler |         // the content is provided by backend IssuePosters handler | ||||||
|         processedResults.length = 0; |         processedResults.length = 0; | ||||||
|         for (const item of resp.results) { |         for (const item of resp.results) { | ||||||
| @@ -153,7 +154,7 @@ function initDropdownUserRemoteSearch(el: Element) { | |||||||
|   const dropdownSetup = {...$searchDropdown.dropdown('internal', 'setup')}; |   const dropdownSetup = {...$searchDropdown.dropdown('internal', 'setup')}; | ||||||
|   const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates'); |   const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates'); | ||||||
|   $searchDropdown.dropdown('internal', 'setup', dropdownSetup); |   $searchDropdown.dropdown('internal', 'setup', dropdownSetup); | ||||||
|   dropdownSetup.menu = function (values) { |   dropdownSetup.menu = function (values: any) { | ||||||
|     // remove old dynamic items |     // remove old dynamic items | ||||||
|     for (const el of elMenu.querySelectorAll(':scope > .dynamic-item')) { |     for (const el of elMenu.querySelectorAll(':scope > .dynamic-item')) { | ||||||
|       el.remove(); |       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 url = e.item.getAttribute('data-move-url'); | ||||||
|   const id = Number(e.item.getAttribute('data-issue-id')); |   const id = Number(e.item.getAttribute('data-issue-id')); | ||||||
|   await POST(url, {data: {id, position: e.newIndex + 1}}); |   await POST(url, {data: {id, position: e.newIndex + 1}}); | ||||||
|   | |||||||
| @@ -46,7 +46,7 @@ class IssueSidebarComboList { | |||||||
|     return Array.from(this.elDropdown.querySelectorAll('.menu > .item.checked'), (el) => el.getAttribute('data-value')); |     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'); |     const elEmptyTip = this.elList.querySelector('.item.empty-list'); | ||||||
|     queryElemChildren(this.elList, '.item:not(.empty-list)', (el) => el.remove()); |     queryElemChildren(this.elList, '.item:not(.empty-list)', (el) => el.remove()); | ||||||
|     for (const value of changedValues) { |     for (const value of changedValues) { | ||||||
| @@ -60,7 +60,7 @@ class IssueSidebarComboList { | |||||||
|     toggleElem(elEmptyTip, !hasItems); |     toggleElem(elEmptyTip, !hasItems); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   async updateToBackend(changedValues) { |   async updateToBackend(changedValues: Array<string>) { | ||||||
|     if (this.updateAlgo === 'diff') { |     if (this.updateAlgo === 'diff') { | ||||||
|       for (const value of this.initialValues) { |       for (const value of this.initialValues) { | ||||||
|         if (!changedValues.includes(value)) { |         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'); |     const elItem = (e.target as HTMLElement).closest('.item'); | ||||||
|     if (!elItem) return; |     if (!elItem) return; | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|   | |||||||
| @@ -32,8 +32,8 @@ export function initRepoIssueSidebarList() { | |||||||
|     fullTextSearch: true, |     fullTextSearch: true, | ||||||
|     apiSettings: { |     apiSettings: { | ||||||
|       url: issueSearchUrl, |       url: issueSearchUrl, | ||||||
|       onResponse(response) { |       onResponse(response: any) { | ||||||
|         const filteredResponse = {success: true, results: []}; |         const filteredResponse = {success: true, results: [] as Array<Record<string, any>>}; | ||||||
|         const currIssueId = $('#new-dependency-drop-list').data('issue-id'); |         const currIssueId = $('#new-dependency-drop-list').data('issue-id'); | ||||||
|         // Parse the response from the api to work with our dropdown |         // Parse the response from the api to work with our dropdown | ||||||
|         $.each(response, (_i, issue) => { |         $.each(response, (_i, issue) => { | ||||||
| @@ -247,7 +247,7 @@ export function initRepoPullRequestUpdate() { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   $('.update-button > .dropdown').dropdown({ |   $('.update-button > .dropdown').dropdown({ | ||||||
|     onChange(_text, _value, $choice) { |     onChange(_text: string, _value: string, $choice: any) { | ||||||
|       const choiceEl = $choice[0]; |       const choiceEl = $choice[0]; | ||||||
|       const url = choiceEl.getAttribute('data-do'); |       const url = choiceEl.getAttribute('data-do'); | ||||||
|       if (url) { |       if (url) { | ||||||
| @@ -298,8 +298,8 @@ export function initRepoIssueReferenceRepositorySearch() { | |||||||
|     .dropdown({ |     .dropdown({ | ||||||
|       apiSettings: { |       apiSettings: { | ||||||
|         url: `${appSubUrl}/repo/search?q={query}&limit=20`, |         url: `${appSubUrl}/repo/search?q={query}&limit=20`, | ||||||
|         onResponse(response) { |         onResponse(response: any) { | ||||||
|           const filteredResponse = {success: true, results: []}; |           const filteredResponse = {success: true, results: [] as Array<Record<string, any>>}; | ||||||
|           $.each(response.data, (_r, repo) => { |           $.each(response.data, (_r, repo) => { | ||||||
|             filteredResponse.results.push({ |             filteredResponse.results.push({ | ||||||
|               name: htmlEscape(repo.repository.full_name), |               name: htmlEscape(repo.repository.full_name), | ||||||
| @@ -310,7 +310,7 @@ export function initRepoIssueReferenceRepositorySearch() { | |||||||
|         }, |         }, | ||||||
|         cache: false, |         cache: false, | ||||||
|       }, |       }, | ||||||
|       onChange(_value, _text, $choice) { |       onChange(_value: string, _text: string, $choice: any) { | ||||||
|         const $form = $choice.closest('form'); |         const $form = $choice.closest('form'); | ||||||
|         if (!$form.length) return; |         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 form = el.closest('.comment-code-cloud').querySelector('.comment-form'); | ||||||
|   const textarea = form.querySelector('textarea'); |   const textarea = form.querySelector('textarea'); | ||||||
|  |  | ||||||
| @@ -379,7 +379,7 @@ export function initRepoPullRequestReview() { | |||||||
|       const groupID = commentDiv.closest('div[id^="code-comments-"]')?.getAttribute('id'); |       const groupID = commentDiv.closest('div[id^="code-comments-"]')?.getAttribute('id'); | ||||||
|       if (groupID && groupID.startsWith('code-comments-')) { |       if (groupID && groupID.startsWith('code-comments-')) { | ||||||
|         const id = groupID.slice(14); |         const id = groupID.slice(14); | ||||||
|         const ancestorDiffBox = commentDiv.closest('.diff-file-box'); |         const ancestorDiffBox = commentDiv.closest<HTMLElement>('.diff-file-box'); | ||||||
|  |  | ||||||
|         hideElem(`#show-outdated-${id}`); |         hideElem(`#show-outdated-${id}`); | ||||||
|         showElem(`#code-comments-${id}, #code-preview-${id}, #hide-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: |   // pages: | ||||||
|   // * normal new issue/pr page: no status-button, no comment-button (there is only a normal submit button which can submit empty content) |   // * 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 |   // * issue/pr view page: with comment form, has status-button and comment-button | ||||||
| @@ -611,7 +611,7 @@ async function initSingleCommentEditor($commentForm) { | |||||||
|   syncUiState(); |   syncUiState(); | ||||||
| } | } | ||||||
|  |  | ||||||
| function initIssueTemplateCommentEditors($commentForm) { | function initIssueTemplateCommentEditors($commentForm: any) { | ||||||
|   // pages: |   // pages: | ||||||
|   // * new issue with issue template |   // * new issue with issue template | ||||||
|   const $comboFields = $commentForm.find('.combo-editor-dropzone'); |   const $comboFields = $commentForm.find('.combo-editor-dropzone'); | ||||||
|   | |||||||
| @@ -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'; | import {GET, POST} from '../modules/fetch.ts'; | ||||||
|  |  | ||||||
| export function initRepoMigrationStatusChecker() { | export function initRepoMigrationStatusChecker() { | ||||||
|   const repoMigrating = document.querySelector('#repo_migrating'); |   const repoMigrating = document.querySelector('#repo_migrating'); | ||||||
|   if (!repoMigrating) return; |   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'); |   const repoLink = repoMigrating.getAttribute('data-migrating-repo-link'); | ||||||
|  |  | ||||||
| @@ -55,7 +55,7 @@ export function initRepoMigrationStatusChecker() { | |||||||
|   syncTaskStatus(); // no await |   syncTaskStatus(); // no await | ||||||
| } | } | ||||||
|  |  | ||||||
| async function doMigrationRetry(e) { | async function doMigrationRetry(e: DOMEvent<MouseEvent>) { | ||||||
|   await POST(e.target.getAttribute('data-migrating-task-retry-url')); |   await POST(e.target.getAttribute('data-migrating-task-retry-url')); | ||||||
|   window.location.reload(); |   window.location.reload(); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -23,7 +23,7 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) { | |||||||
|     $dropdown.dropdown('setting', { |     $dropdown.dropdown('setting', { | ||||||
|       apiSettings: { |       apiSettings: { | ||||||
|         url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${inputRepoOwnerUid.value}`, |         url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${inputRepoOwnerUid.value}`, | ||||||
|         onResponse(response) { |         onResponse(response: any) { | ||||||
|           const results = []; |           const results = []; | ||||||
|           results.push({name: '', value: ''}); // empty item means not using template |           results.push({name: '', value: ''}); // empty item means not using template | ||||||
|           for (const tmplRepo of response.data) { |           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)}"]`); |     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=""]`); |     if (!help) help = form.querySelector(`.help[data-help-for-repo-name=""]`); | ||||||
|     showElem(help); |     showElem(help); | ||||||
|     const repoNamePreferPrivate = {'.profile': false, '.profile-private': true}; |     const repoNamePreferPrivate: Record<string, boolean> = {'.profile': false, '.profile-private': true}; | ||||||
|     const preferPrivate = repoNamePreferPrivate[inputRepoName.value]; |     const preferPrivate = repoNamePreferPrivate[inputRepoName.value]; | ||||||
|     // inputPrivate might be disabled because site admin "force private" |     // inputPrivate might be disabled because site admin "force private" | ||||||
|     if (preferPrivate !== undefined && !inputPrivate.closest('.disabled, [disabled]')) { |     if (preferPrivate !== undefined && !inputPrivate.closest('.disabled, [disabled]')) { | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ function initRepoSettingsCollaboration() { | |||||||
|   for (const dropdownEl of queryElems(document, '.page-content.repository .ui.dropdown.access-mode')) { |   for (const dropdownEl of queryElems(document, '.page-content.repository .ui.dropdown.access-mode')) { | ||||||
|     const textEl = dropdownEl.querySelector(':scope > .text'); |     const textEl = dropdownEl.querySelector(':scope > .text'); | ||||||
|     $(dropdownEl).dropdown({ |     $(dropdownEl).dropdown({ | ||||||
|       async action(text, value) { |       async action(text: string, value: string) { | ||||||
|         dropdownEl.classList.add('is-loading', 'loading-icon-2px'); |         dropdownEl.classList.add('is-loading', 'loading-icon-2px'); | ||||||
|         const lastValue = dropdownEl.getAttribute('data-last-value'); |         const lastValue = dropdownEl.getAttribute('data-last-value'); | ||||||
|         $(dropdownEl).dropdown('hide'); |         $(dropdownEl).dropdown('hide'); | ||||||
| @@ -53,8 +53,8 @@ function initRepoSettingsSearchTeamBox() { | |||||||
|     apiSettings: { |     apiSettings: { | ||||||
|       url: `${appSubUrl}/org/${searchTeamBox.getAttribute('data-org-name')}/teams/-/search?q={query}`, |       url: `${appSubUrl}/org/${searchTeamBox.getAttribute('data-org-name')}/teams/-/search?q={query}`, | ||||||
|       headers: {'X-Csrf-Token': csrfToken}, |       headers: {'X-Csrf-Token': csrfToken}, | ||||||
|       onResponse(response) { |       onResponse(response: any) { | ||||||
|         const items = []; |         const items: Array<Record<string, any>> = []; | ||||||
|         $.each(response.data, (_i, item) => { |         $.each(response.data, (_i, item) => { | ||||||
|           items.push({ |           items.push({ | ||||||
|             title: item.name, |             title: item.name, | ||||||
|   | |||||||
| @@ -70,7 +70,7 @@ async function initRepoWikiFormEditor() { | |||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function collapseWikiTocForMobile(collapse) { | function collapseWikiTocForMobile(collapse: boolean) { | ||||||
|   if (collapse) { |   if (collapse) { | ||||||
|     document.querySelector('.wiki-content-toc details')?.removeAttribute('open'); |     document.querySelector('.wiki-content-toc details')?.removeAttribute('open'); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -38,7 +38,7 @@ export function initStopwatch() { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   let usingPeriodicPoller = false; |   let usingPeriodicPoller = false; | ||||||
|   const startPeriodicPoller = (timeout) => { |   const startPeriodicPoller = (timeout: number) => { | ||||||
|     if (timeout <= 0 || !Number.isFinite(timeout)) return; |     if (timeout <= 0 || !Number.isFinite(timeout)) return; | ||||||
|     usingPeriodicPoller = true; |     usingPeriodicPoller = true; | ||||||
|     setTimeout(() => updateStopwatchWithCallback(startPeriodicPoller, timeout), timeout); |     setTimeout(() => updateStopwatchWithCallback(startPeriodicPoller, timeout), timeout); | ||||||
| @@ -103,7 +103,7 @@ export function initStopwatch() { | |||||||
|   startPeriodicPoller(notificationSettings.MinTimeout); |   startPeriodicPoller(notificationSettings.MinTimeout); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function updateStopwatchWithCallback(callback, timeout) { | async function updateStopwatchWithCallback(callback: (timeout: number) => void, timeout: number) { | ||||||
|   const isSet = await updateStopwatch(); |   const isSet = await updateStopwatch(); | ||||||
|  |  | ||||||
|   if (!isSet) { |   if (!isSet) { | ||||||
| @@ -125,7 +125,7 @@ async function updateStopwatch() { | |||||||
|   return updateStopwatchData(data); |   return updateStopwatchData(data); | ||||||
| } | } | ||||||
|  |  | ||||||
| function updateStopwatchData(data) { | function updateStopwatchData(data: any) { | ||||||
|   const watch = data[0]; |   const watch = data[0]; | ||||||
|   const btnEls = document.querySelectorAll('.active-stopwatch'); |   const btnEls = document.querySelectorAll('.active-stopwatch'); | ||||||
|   if (!watch) { |   if (!watch) { | ||||||
|   | |||||||
| @@ -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 (!normSort) return false; | ||||||
|   if (!revSort) revSort = ''; |   if (!revSort) revSort = ''; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,14 +1,16 @@ | |||||||
| import {emojiKeys, emojiHTML, emojiString} from './emoji.ts'; | import {emojiKeys, emojiHTML, emojiString} from './emoji.ts'; | ||||||
| import {htmlEscape} from 'escape-goat'; | import {htmlEscape} from 'escape-goat'; | ||||||
|  |  | ||||||
| function makeCollections({mentions, emoji}) { | type TributeItem = Record<string, any>; | ||||||
|   const collections = []; |  | ||||||
|  |  | ||||||
|   if (emoji) { | export async function attachTribute(element: HTMLElement) { | ||||||
|     collections.push({ |   const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs'); | ||||||
|  |  | ||||||
|  |   const collections = [ | ||||||
|  |     { // emojis | ||||||
|       trigger: ':', |       trigger: ':', | ||||||
|       requireLeadingSpace: true, |       requireLeadingSpace: true, | ||||||
|       values: (query, cb) => { |       values: (query: string, cb: (matches: Array<string>) => void) => { | ||||||
|         const matches = []; |         const matches = []; | ||||||
|         for (const name of emojiKeys) { |         for (const name of emojiKeys) { | ||||||
|           if (name.includes(query)) { |           if (name.includes(query)) { | ||||||
| @@ -18,22 +20,18 @@ function makeCollections({mentions, emoji}) { | |||||||
|         } |         } | ||||||
|         cb(matches); |         cb(matches); | ||||||
|       }, |       }, | ||||||
|       lookup: (item) => item, |       lookup: (item: TributeItem) => item, | ||||||
|       selectTemplate: (item) => { |       selectTemplate: (item: TributeItem) => { | ||||||
|         if (item === undefined) return null; |         if (item === undefined) return null; | ||||||
|         return emojiString(item.original); |         return emojiString(item.original); | ||||||
|       }, |       }, | ||||||
|       menuItemTemplate: (item) => { |       menuItemTemplate: (item: TributeItem) => { | ||||||
|         return `<div class="tribute-item">${emojiHTML(item.original)}<span>${htmlEscape(item.original)}</span></div>`; |         return `<div class="tribute-item">${emojiHTML(item.original)}<span>${htmlEscape(item.original)}</span></div>`; | ||||||
|       }, |       }, | ||||||
|     }); |     }, { // mentions | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (mentions) { |  | ||||||
|     collections.push({ |  | ||||||
|       values: window.config.mentionValues ?? [], |       values: window.config.mentionValues ?? [], | ||||||
|       requireLeadingSpace: true, |       requireLeadingSpace: true, | ||||||
|       menuItemTemplate: (item) => { |       menuItemTemplate: (item: TributeItem) => { | ||||||
|         return ` |         return ` | ||||||
|           <div class="tribute-item"> |           <div class="tribute-item"> | ||||||
|             <img src="${htmlEscape(item.original.avatar)}" width="21" height="21"/> |             <img src="${htmlEscape(item.original.avatar)}" width="21" height="21"/> | ||||||
| @@ -42,15 +40,9 @@ function makeCollections({mentions, emoji}) { | |||||||
|           </div> |           </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) |   // @ts-expect-error TS2351: This expression is not constructable (strange, why) | ||||||
|   const tribute = new Tribute({collection: collections, noMatchTemplate: ''}); |   const tribute = new Tribute({collection: collections, noMatchTemplate: ''}); | ||||||
|   tribute.attach(element); |   tribute.attach(element); | ||||||
|   | |||||||
| @@ -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 |   // Move data into Arrays in case it is super long | ||||||
|   const authData = new Uint8Array(assertedCredential.response.authenticatorData); |   const authData = new Uint8Array(assertedCredential.response.authenticatorData); | ||||||
|   const clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON); |   const clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON); | ||||||
| @@ -148,7 +148,7 @@ async function verifyAssertion(assertedCredential) { | |||||||
|   window.location.href = reply?.redirect ?? `${appSubUrl}/`; |   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 attestationObject = new Uint8Array(newCredential.response.attestationObject); | ||||||
|   const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); |   const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); | ||||||
|   const rawId = new Uint8Array(newCredential.rawId); |   const rawId = new Uint8Array(newCredential.rawId); | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ export async function renderAsciicast() { | |||||||
|   if (!els.length) return; |   if (!els.length) return; | ||||||
|  |  | ||||||
|   const [player] = await Promise.all([ |   const [player] = await Promise.all([ | ||||||
|  |     // @ts-expect-error: module exports no types | ||||||
|     import(/* webpackChunkName: "asciinema-player" */'asciinema-player'), |     import(/* webpackChunkName: "asciinema-player" */'asciinema-player'), | ||||||
|     import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'), |     import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'), | ||||||
|   ]); |   ]); | ||||||
|   | |||||||
| @@ -1,7 +1,9 @@ | |||||||
| import {htmlEscape} from 'escape-goat'; | import {htmlEscape} from 'escape-goat'; | ||||||
|  |  | ||||||
|  | type Processor = (el: HTMLElement) => string | HTMLElement | void; | ||||||
|  |  | ||||||
| type Processors = { | type Processors = { | ||||||
|   [tagName: string]: (el: HTMLElement) => string | HTMLElement | void; |   [tagName: string]: Processor; | ||||||
| } | } | ||||||
|  |  | ||||||
| type ProcessorContext = { | type ProcessorContext = { | ||||||
| @@ -11,7 +13,7 @@ type ProcessorContext = { | |||||||
| } | } | ||||||
|  |  | ||||||
| function prepareProcessors(ctx:ProcessorContext): Processors { | function prepareProcessors(ctx:ProcessorContext): Processors { | ||||||
|   const processors = { |   const processors: Processors = { | ||||||
|     H1(el: HTMLElement) { |     H1(el: HTMLElement) { | ||||||
|       const level = parseInt(el.tagName.slice(1)); |       const level = parseInt(el.tagName.slice(1)); | ||||||
|       el.textContent = `${'#'.repeat(level)} ${el.textContent.trim()}`; |       el.textContent = `${'#'.repeat(level)} ${el.textContent.trim()}`; | ||||||
|   | |||||||
| @@ -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. | // 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) { | function updateMenuItem(dropdown: HTMLElement, item: HTMLElement) { | ||||||
|   if (!item.id) item.id = generateAriaId(); |   if (!item.id) item.id = generateAriaId(); | ||||||
|   item.setAttribute('role', dropdown[ariaPatchKey].listItemRole); |   item.setAttribute('role', (dropdown as any)[ariaPatchKey].listItemRole); | ||||||
|   item.setAttribute('tabindex', '-1'); |   item.setAttribute('tabindex', '-1'); | ||||||
|   for (const el of item.querySelectorAll('a, input, button')) el.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 hideEmptyDividers = dropdownCall('setting', 'hideDividers') === 'empty'; | ||||||
|   const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu'); |   const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu'); | ||||||
|   if (hideEmptyDividers) hideScopedEmptyDividers(itemsMenu); |   if (hideEmptyDividers) hideScopedEmptyDividers(itemsMenu); | ||||||
| @@ -143,7 +143,7 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men | |||||||
|   $(menu).find('> .item').each((_, item) => updateMenuItem(dropdown, item)); |   $(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 |   // 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 |   // prepare selection label items | ||||||
|   for (const label of dropdown.querySelectorAll<HTMLElement>('.ui.label')) { |   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 |   // make the primary element (focusable) aria-friendly | ||||||
|   focusable.setAttribute('role', focusable.getAttribute('role') ?? dropdown[ariaPatchKey].focusableRole); |   focusable.setAttribute('role', focusable.getAttribute('role') ?? (dropdown as any)[ariaPatchKey].focusableRole); | ||||||
|   focusable.setAttribute('aria-haspopup', dropdown[ariaPatchKey].listPopupRole); |   focusable.setAttribute('aria-haspopup', (dropdown as any)[ariaPatchKey].listPopupRole); | ||||||
|   focusable.setAttribute('aria-controls', menu.id); |   focusable.setAttribute('aria-controls', menu.id); | ||||||
|   focusable.setAttribute('aria-expanded', 'false'); |   focusable.setAttribute('aria-expanded', 'false'); | ||||||
|  |  | ||||||
| @@ -164,7 +164,7 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men | |||||||
| } | } | ||||||
|  |  | ||||||
| function attachInit(dropdown: HTMLElement) { | function attachInit(dropdown: HTMLElement) { | ||||||
|   dropdown[ariaPatchKey] = {}; |   (dropdown as any)[ariaPatchKey] = {}; | ||||||
|   if (dropdown.classList.contains('custom')) return; |   if (dropdown.classList.contains('custom')) return; | ||||||
|  |  | ||||||
|   // Dropdown has 2 different focusing behaviors |   // 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. |   // 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; |   const isComboBox = dropdown.querySelectorAll('input').length > 0; | ||||||
|  |  | ||||||
|   dropdown[ariaPatchKey].focusableRole = isComboBox ? 'combobox' : 'menu'; |   (dropdown as any)[ariaPatchKey].focusableRole = isComboBox ? 'combobox' : 'menu'; | ||||||
|   dropdown[ariaPatchKey].listPopupRole = isComboBox ? 'listbox' : ''; |   (dropdown as any)[ariaPatchKey].listPopupRole = isComboBox ? 'listbox' : ''; | ||||||
|   dropdown[ariaPatchKey].listItemRole = isComboBox ? 'option' : 'menuitem'; |   (dropdown as any)[ariaPatchKey].listItemRole = isComboBox ? 'option' : 'menuitem'; | ||||||
|  |  | ||||||
|   attachDomEvents(dropdown, focusable, menu); |   attachDomEvents(dropdown, focusable, menu); | ||||||
|   attachStaticElements(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 the popup is visible and has an active/selected item, use its id as aria-activedescendant | ||||||
|     if (menuVisible) { |     if (menuVisible) { | ||||||
|       focusable.setAttribute('aria-activedescendant', active.id); |       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 |       // for menu, when the popup is hidden, no need to keep the aria-activedescendant, and clear the active/selected item | ||||||
|       focusable.removeAttribute('aria-activedescendant'); |       focusable.removeAttribute('aria-activedescendant'); | ||||||
|       active.classList.remove('active', 'selected'); |       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 |   // 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. |   // 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) }; |   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(); }); |   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. |   // 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. |   // 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)` |   // 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)` |   // 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(() => { |   setTimeout(() => { | ||||||
|     queryElems(elDropdown, `.menu .item[data-value="${CSS.escape(selectedValue)}"].filtered`, (el) => el.classList.remove('filtered')); |     queryElems(elDropdown, `.menu .item[data-value="${CSS.escape(selectedValue)}"].filtered`, (el) => el.classList.remove('filtered')); | ||||||
|     $(elDropdown).dropdown('set selected', selectedValue ?? ''); |     $(elDropdown).dropdown('set selected', selectedValue ?? ''); | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.ts'; | import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.ts'; | ||||||
|  |  | ||||||
| function initDevtestToast() { | 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')) { |   for (const el of document.querySelectorAll('.toast-test-button')) { | ||||||
|     el.addEventListener('click', () => { |     el.addEventListener('click', () => { | ||||||
|       const level = el.getAttribute('data-toast-level'); |       const level = el.getAttribute('data-toast-level'); | ||||||
|   | |||||||
| @@ -208,7 +208,7 @@ export const SvgIcon = defineComponent({ | |||||||
|     let {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name); |     let {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name); | ||||||
|     // https://vuejs.org/guide/extras/render-function.html#creating-vnodes |     // https://vuejs.org/guide/extras/render-function.html#creating-vnodes | ||||||
|     // the `^` is used for attr, set SVG attributes like 'width', `aria-hidden`, `viewBox`, etc |     // 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) { |     for (const attr of svgOuter.attributes) { | ||||||
|       if (attr.name === 'class') continue; |       if (attr.name === 'class') continue; | ||||||
|       attrs[`^${attr.name}`] = attr.value; |       attrs[`^${attr.name}`] = attr.value; | ||||||
|   | |||||||
| @@ -22,6 +22,8 @@ export type Config = { | |||||||
|   i18n: Record<string, string>, |   i18n: Record<string, string>, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export type IntervalId = ReturnType<typeof setInterval>; | ||||||
|  |  | ||||||
| export type Intent = 'error' | 'warning' | 'info'; | export type Intent = 'error' | 'warning' | 'info'; | ||||||
|  |  | ||||||
| export type RequestData = string | FormData | URLSearchParams | Record<string, any>; | export type RequestData = string | FormData | URLSearchParams | Record<string, any>; | ||||||
|   | |||||||
| @@ -166,10 +166,10 @@ export function sleep(ms: number): Promise<void> { | |||||||
|   return new Promise((resolve) => setTimeout(resolve, ms)); |   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/'); |   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/'); |   return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/'); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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)" | // it can't use other transparent polyfill patches because PaleMoon also doesn't support "addEventListener(capture)" | ||||||
| const needSubmitEventPolyfill = typeof SubmitEvent === 'undefined'; | 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 |   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; |   return needSubmitEventPolyfill ? (e.target._submitter || null) : e.submitter; | ||||||
| } | } | ||||||
|  |  | ||||||
| function submitEventPolyfillListener(e) { | function submitEventPolyfillListener(e: DOMEvent<Event>) { | ||||||
|   const form = e.target.closest('form'); |   const form = e.target.closest('form'); | ||||||
|   if (!form) return; |   if (!form) return; | ||||||
|   form._submitter = e.target.closest('button:not([type]), button[type="submit"], input[type="submit"]'); |   form._submitter = e.target.closest('button:not([type]), button[type="submit"], input[type="submit"]'); | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA | |||||||
| const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A=='; | const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A=='; | ||||||
| const pngEmpty = 'data:image/png;base64,'; | const pngEmpty = 'data:image/png;base64,'; | ||||||
|  |  | ||||||
| async function dataUriToBlob(datauri) { | async function dataUriToBlob(datauri: string) { | ||||||
|   return await (await globalThis.fetch(datauri)).blob(); |   return await (await globalThis.fetch(datauri)).blob(); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -54,7 +54,7 @@ export type DayDataObject = { | |||||||
| } | } | ||||||
|  |  | ||||||
| export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayDataObject): DayData[] { | export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayDataObject): DayData[] { | ||||||
|   const result = {}; |   const result: Record<string, any> = {}; | ||||||
|  |  | ||||||
|   for (const startDay of startDays) { |   for (const startDay of startDays) { | ||||||
|     result[startDay] = data[startDay] || {'week': startDay, 'additions': 0, 'deletions': 0, 'commits': 0}; |     result[startDay] = data[startDay] || {'week': startDay, 'additions': 0, 'deletions': 0, 'commits': 0}; | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ window.customElements.define('absolute-date', class extends HTMLElement { | |||||||
|   initialized = false; |   initialized = false; | ||||||
|  |  | ||||||
|   update = () => { |   update = () => { | ||||||
|     const opt: Intl.DateTimeFormatOptions = {}; |     const opt: Record<string, string> = {}; | ||||||
|     for (const attr of ['year', 'month', 'weekday', 'day']) { |     for (const attr of ['year', 'month', 'weekday', 'day']) { | ||||||
|       if (this.getAttribute(attr)) opt[attr] = this.getAttribute(attr); |       if (this.getAttribute(attr)) opt[attr] = this.getAttribute(attr); | ||||||
|     } |     } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user