chore: upgrade eslint plugins, remove eslint-plugin-github (#38046)

- Bump `eslint`, `typescript-eslint` and `eslint-plugin-unicorn` (to
v68), and configure the rules added in unicorn v66/v67/v68.
- Remove `eslint-plugin-github` and its workarounds (rules, type stub,
pnpm peer override, in-code `eslint-disable` comments); the rules worth
keeping are covered by `unicorn` equivalents.
- Apply the resulting fixes and autofixes across the JS codebase.

_Prepared with Claude (Opus 4.8)._

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwind
2026-06-21 08:07:13 +02:00
committed by GitHub
co-authored by wxiaoguang
parent 5368542f8e
commit 804b9bf120
54 changed files with 622 additions and 840 deletions
+2 -2
View File
@@ -47,9 +47,9 @@ export type LogLineCommand = {
export function parseLogLineCommand(line: LogLine): LogLineCommand | null {
// TODO: in the future it can be refactored to be a general parser that can parse arguments, drop the "prefix match"
for (const prefix of Object.keys(LogLinePrefixCommandMap)) {
for (const [prefix, commandName] of Object.entries(LogLinePrefixCommandMap)) {
if (line.message.startsWith(prefix)) {
return {name: LogLinePrefixCommandMap[prefix], prefix};
return {name: commandName, prefix};
}
}
// Handle ::cmd:: and ::cmd args:: format (runner may pass these through raw)
+1 -3
View File
@@ -57,9 +57,7 @@ export function createViewFileTreeStore(props: {repoLink: string, treePath: stri
await store.loadViewContent(url);
},
buildTreePathWebUrl(treePath: string) {
return `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`;
},
buildTreePathWebUrl: (treePath: string) => `${props.repoLink}/src/${props.currentRefNameSubURL}/${pathEscapeSegments(treePath)}`,
});
return store;
}
+1 -1
View File
@@ -166,7 +166,7 @@ function buildDirectNeedsMap(jobs: ActionsJob[]): Map<string, string[]> {
const reducedNeedsByJobId = new Map<string, string[]>();
for (const [jobId, needs] of directNeedsByJobId) {
reducedNeedsByJobId.set(jobId, needs.filter((need) => {
return !needs.some((other) => other !== need && canReach(need, other));
return needs.every((other) => other === need || !canReach(need, other));
}));
}
return reducedNeedsByJobId;
+2 -2
View File
@@ -30,9 +30,9 @@ test('ConfigFormValueMapper', () => {
const formData = mapper.collectToFormData();
const result: Record<string, string> = {};
const keys: string[] = [], values: string[] = [];
for (const [key, value] of formData.entries()) {
for (const [key, value] of formData) {
if (key === 'key') keys.push(value as string);
if (key === 'value') values.push(value as string);
else if (key === 'value') values.push(value as string);
}
for (let i = 0; i < keys.length; i++) {
result[keys[i]] = values[i];
+3 -7
View File
@@ -102,9 +102,7 @@ export class ConfigFormValueMapper {
} else if (el.matches('[type="datetime-local"]')) {
if (valType !== 'timestamp') requireExplicitValueType(el);
if (val) el.value = toDatetimeLocalValue(val);
} else if (el.matches('textarea')) {
el.value = String(val ?? el.value);
} else if (el.matches('input') && (el.getAttribute('type') ?? 'text') === 'text') {
} else if (el.matches('textarea') || (el.matches('input') && (el.getAttribute('type') ?? 'text') === 'text')) {
el.value = String(val ?? el.value);
} else {
unsupportedElement(el);
@@ -123,9 +121,7 @@ export class ConfigFormValueMapper {
} else if (el.matches('[type="datetime-local"]')) {
if (valType !== 'timestamp') requireExplicitValueType(el);
val = Math.floor(new Date(el.value).getTime() / 1000) ?? 0; // NaN is fine to JSON.stringify, it becomes null.
} else if (el.matches('textarea')) {
val = el.value;
} else if (el.matches('input') && (el.getAttribute('type') ?? 'text') === 'text') {
} else if (el.matches('textarea') || (el.matches('input') && (el.getAttribute('type') ?? 'text') === 'text')) {
val = el.value;
} else {
unsupportedElement(el);
@@ -178,7 +174,7 @@ export class ConfigFormValueMapper {
collectToFormData(): FormData {
const namedElems: Array<GeneralFormFieldElement | null> = [];
queryElems(this.form, '[name]', (el) => namedElems.push(el as GeneralFormFieldElement));
queryElems(this.form, '[name]', (el) => { namedElems.push(el as GeneralFormFieldElement) });
// first, process the config options with sub values, for example:
// merge "foo.bar.Enabled", "foo.bar.Message" to "foo.bar"
+2 -2
View File
@@ -5,14 +5,14 @@ export function initAdminUserListSearchForm(): void {
const form = document.querySelector<HTMLFormElement>('#user-list-search-form');
if (!form) return;
for (const button of form.querySelectorAll(`button[name=sort][value="${searchForm.SortType}"]`)) {
for (const button of form.querySelectorAll(`button[name=sort][value="${CSS.escape(searchForm.SortType)}"]`)) {
button.classList.add('active');
}
if (searchForm.StatusFilterMap) {
for (const [k, v] of Object.entries(searchForm.StatusFilterMap)) {
if (!v) continue;
for (const input of form.querySelectorAll<HTMLInputElement>(`input[name="status_filter[${k}]"][value="${v}"]`)) {
for (const input of form.querySelectorAll<HTMLInputElement>(`input[name="status_filter[${CSS.escape(k)}]"][value="${CSS.escape(v)}"]`)) {
input.checked = true;
}
}
+3 -3
View File
@@ -87,10 +87,10 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
const [attrTargetName, attrTargetProp] = attrTargetCombo.split('.');
// try to find target by: "#target" -> "[name=target]" -> ".target" -> "<target> tag", and then try the modal itself
const attrTarget = elModal.querySelector(`#${attrTargetName}`) ||
elModal.querySelector(`[name=${attrTargetName}]`) ||
elModal.querySelector(`[name=${CSS.escape(attrTargetName)}]`) ||
elModal.querySelector(`.${attrTargetName}`) ||
elModal.querySelector(`${attrTargetName}`) ||
(elModal.matches(`${attrTargetName}`) || elModal.matches(`#${attrTargetName}`) || elModal.matches(`.${attrTargetName}`) ? elModal : null);
elModal.querySelector(attrTargetName) ||
(elModal.matches(attrTargetName) || elModal.matches(`#${attrTargetName}`) || elModal.matches(`.${attrTargetName}`) ? elModal : null);
if (!attrTarget) {
if (!window.config.runModeIsProd) throw new Error(`attr target "${attrTargetCombo}" not found for modal`);
continue;
+1 -1
View File
@@ -114,7 +114,7 @@ function buildFetchActionUrl(el: HTMLElement, opt: FetchActionOpts) {
const u = new URL(url, window.location.href);
if (name && !u.searchParams.has(name)) {
u.searchParams.set(name, val);
url = u.toString();
url = u.href;
}
}
return url;
+6 -10
View File
@@ -16,17 +16,13 @@ export function initGlobalEnterQuickSubmit() {
document.addEventListener('keydown', (e) => {
if (e.isComposing) return;
if (e.key !== 'Enter') return;
const el = e.target as HTMLElement;
const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
if (hasCtrlOrMeta && (e.target as HTMLElement).matches('textarea')) {
if (handleGlobalEnterQuickSubmit(e.target as HTMLElement)) {
e.preventDefault();
}
} else if ((e.target as HTMLElement).matches('input') && !(e.target as HTMLElement).closest('form')) {
// input in a normal form could handle Enter key by default, so we only handle the input outside a form
// eslint-disable-next-line unicorn/no-lonely-if
if (handleGlobalEnterQuickSubmit(e.target as HTMLElement)) {
e.preventDefault();
}
const isCtrlEnterInTextarea = hasCtrlOrMeta && el.matches('textarea');
// an input in a normal form could handle Enter key by default, so we only handle the input outside a form
const isEnterInBareInput = el.matches('input') && !el.closest('form');
if ((isCtrlEnterInTextarea || isEnterInBareInput) && handleGlobalEnterQuickSubmit(el)) {
e.preventDefault();
}
});
}
+1 -1
View File
@@ -35,7 +35,7 @@ export function initCommonIssueListQuickGoto() {
const repoLink = elGotoButton.getAttribute('data-repo-link') || '';
elGotoButton.addEventListener('click', () => {
window.location.href = elGotoButton.getAttribute('data-issue-goto-link')!;
window.location.assign(elGotoButton.getAttribute('data-issue-goto-link')!);
});
const onInput = async () => {
+2 -2
View File
@@ -110,8 +110,8 @@ export async function initDropzone(dropzoneEl: HTMLElement) {
});
dzInst.on('submit', () => {
for (const fileUuid of Object.keys(fileUuidDict)) {
fileUuidDict[fileUuid].submitted = true;
for (const value of Object.values(fileUuidDict)) {
value.submitted = true;
}
});
+2 -2
View File
@@ -24,8 +24,8 @@ export async function initHeatmap() {
heatmap[dateStr] = (heatmap[dateStr] || 0) + contributions;
}
const values = Object.keys(heatmap).map((v) => {
return {date: new Date(v), count: heatmap[v]};
const values = Object.entries(heatmap).map(([dateStr, count]) => {
return {date: new Date(dateStr), count};
});
const totalFormatted = totalContributions.toLocaleString();
+1 -1
View File
@@ -291,7 +291,7 @@ class ImageDiff {
function updateOpacity() {
if (ctx.imageAfter) {
(ctx.imageAfter.parentNode as HTMLElement).style.opacity = `${Number(rangeInput.value) / 100}`;
(ctx.imageAfter.parentNode as HTMLElement).style.opacity = String(Number(rangeInput.value) / 100);
}
}
+1 -1
View File
@@ -95,7 +95,7 @@ function initPostInstall() {
if (tid && resp.status === 200) {
clearInterval(tid);
tid = null;
window.location.href = targetUrl;
window.location.assign(targetUrl);
}
} catch {}
}, 1000);
+2 -2
View File
@@ -10,7 +10,7 @@ async function receiveUpdateCount(event: MessageEvent<{type: string, data: strin
const data = JSON.parse(event.data.data);
for (const count of document.querySelectorAll('.notification_count')) {
count.classList.toggle('tw-hidden', data.Count === 0);
count.textContent = `${data.Count}`;
count.textContent = String(data.Count);
}
await updateNotificationTable();
} catch (error) {
@@ -112,7 +112,7 @@ async function updateNotificationCount(): Promise<number> {
toggleElem('.notification_count', data.new !== 0);
for (const el of document.querySelectorAll('.notification_count')) {
el.textContent = `${data.new}`;
el.textContent = String(data.new);
}
return data.new as number;
+2 -2
View File
@@ -31,9 +31,9 @@ function selectRange(range: string): Element | null {
const updateViewGitBlameFragment = function (anchor: string) {
if (!viewGitBlame) return;
let href = viewGitBlame.getAttribute('href')!;
href = `${href.replace(/#L\d+$|#L\d+-L\d+$/, '')}`;
href = href.replace(/#L\d+$|#L\d+-L\d+$/, '');
if (anchor.length !== 0) {
href = `${href}#${anchor}`;
href += `#${anchor}`;
}
viewGitBlame.setAttribute('href', href);
};
+2 -2
View File
@@ -45,8 +45,8 @@ export function initCommitFileHistoryFollowRename() {
registerGlobalInitFunc('initCommitHistoryFollowRename', (el: HTMLInputElement) => {
el.addEventListener('change', () => {
const url = new URL(window.location.toString());
url.searchParams.set('follow-rename', `${el.checked}`);
window.location.assign(url.toString());
url.searchParams.set('follow-rename', String(el.checked));
window.location.assign(url.href);
});
});
}
+1 -1
View File
@@ -23,7 +23,7 @@ async function onDownloadArchive(e: Event) {
if (data.complete) break;
await sleep(Math.min((tryCount + 1) * 750, 2000));
}
window.location.href = el.href; // the archive is ready, start real downloading
window.location.assign(el.href); // the archive is ready, start real downloading
} catch (e) {
console.error(e);
showErrorToast(`Failed to download the archive: ${errorMessage(e)}`, {duration: 2500});
+2 -2
View File
@@ -129,7 +129,7 @@ function initRepoDiffConversationNav() {
const navIndex = isPrevious ? previousIndex : nextIndex;
const elNavConversation = elAllConversations[navIndex];
const anchor = elNavConversation.querySelector('.comment')!.id;
window.location.href = `#${anchor}`;
window.location.assign(`#${anchor}`);
});
}
@@ -245,7 +245,7 @@ async function onLocationHashChange() {
const issueCommentPrefix = '#issuecomment-';
if (currentHash.startsWith(issueCommentPrefix)) {
const commentId = currentHash.substring(issueCommentPrefix.length);
const expandButton = document.querySelector<HTMLElement>(`.code-expander-button[data-hidden-comment-ids*=",${commentId},"]`);
const expandButton = document.querySelector<HTMLElement>(`.code-expander-button[data-hidden-comment-ids*=",${CSS.escape(commentId)},"]`);
if (expandButton) {
// avoid infinite loop, do not re-click the button if already clicked
const attrAutoLoadClicked = 'data-auto-load-clicked';
+2 -2
View File
@@ -42,7 +42,7 @@ export function initRepoGraphGit() {
elGraphBody.classList.add('is-loading');
try {
const resp = await GET(ajaxUrl.toString());
const resp = await GET(ajaxUrl.href);
elGraphBody.innerHTML = await resp.text();
} finally {
elGraphBody.classList.remove('is-loading');
@@ -51,7 +51,7 @@ export function initRepoGraphGit() {
const dropdownSelected = params.getAll('branch');
if (params.has('hide-pr-refs') && params.get('hide-pr-refs') === 'true') {
dropdownSelected.splice(0, 0, '...flow-hide-pr-refs');
dropdownSelected.unshift('...flow-hide-pr-refs');
}
const $dropdown = fomanticQuery('#flow-select-refs-dropdown');
+1 -4
View File
@@ -96,10 +96,7 @@ export function initRepoTopicBar() {
};
const query = stripTags(this.urlData.query.trim());
let found_query = false;
const current_topics = [];
for (const el of queryElemChildren(topicDropdown, 'a.ui.label.visible')) {
current_topics.push(el.getAttribute('data-value'));
}
const current_topics = Array.from(queryElemChildren(topicDropdown, 'a.ui.label.visible'), (el) => el.getAttribute('data-value'));
if (res.topics) {
let found = false;
+1 -1
View File
@@ -148,7 +148,7 @@ export async function initRepoIssueContentHistory() {
if (resp.editedHistoryCountMap[0] && elIssueDescription) {
showContentHistoryMenu(issueBaseUrl, elIssueDescription, '0');
}
for (const [commentId, _editedCount] of Object.entries(resp.editedHistoryCountMap)) {
for (const commentId of Object.keys(resp.editedHistoryCountMap)) {
if (commentId === '0') continue;
const elIssueComment = document.querySelector(`#issuecomment-${commentId}`);
if (elIssueComment) showContentHistoryMenu(issueBaseUrl, elIssueComment, commentId);
+3 -6
View File
@@ -58,10 +58,7 @@ function initRepoIssueListCheckboxes() {
const url = el.getAttribute('data-url')!;
let action = el.getAttribute('data-action')!;
let elementId = el.getAttribute('data-element-id')!;
const issueIDList: string[] = [];
for (const el of document.querySelectorAll('.issue-checkbox:checked')) {
issueIDList.push(el.getAttribute('data-issue-id')!);
}
const issueIDList: string[] = Array.from(document.querySelectorAll('.issue-checkbox:checked'), (el) => (el.getAttribute('data-issue-id')!));
const issueIDs = issueIDList.join(',');
if (!issueIDs) return;
@@ -109,7 +106,7 @@ function initDropdownUserRemoteSearch(el: Element) {
fullTextSearch: true,
selectOnKeydown: false,
action: (_text: string, value: string) => {
window.location.href = actionJumpUrl.replace('{username}', encodeURIComponent(value));
window.location.assign(actionJumpUrl.replace('{username}', encodeURIComponent(value)));
},
});
@@ -192,7 +189,7 @@ function initPinRemoveButton() {
// Delete the tooltip
el._tippy.destroy();
// Remove the Card
el.closest(`div.issue-card[data-issue-id="${id}"]`)!.remove();
el.closest(`div.issue-card[data-issue-id="${CSS.escape(String(id))}"]`)!.remove();
}
});
}
+3 -3
View File
@@ -28,7 +28,7 @@ function initRepoIssueLabelFilter(elDropdown: HTMLElement) {
const queryLabels = url.searchParams.get('labels') || '';
const selectedLabelIds = new Set<string>();
for (const id of queryLabels ? queryLabels.split(',') : []) {
selectedLabelIds.add(`${Math.abs(parseInt(id))}`); // "labels" contains negative ids, which are excluded
selectedLabelIds.add(String(Math.abs(parseInt(id)))); // "labels" contains negative ids, which are excluded
}
const excludeLabel = (e: MouseEvent | KeyboardEvent, item: Element) => {
@@ -130,9 +130,9 @@ export function initRepoIssueCommentDelete() {
// on the Conversation page, there is no parent "tr", so no need to do anything for "add-code-comment"
if (lineType) {
if (lineType === 'same') {
document.querySelector(`[data-path="${path}"] .add-code-comment[data-idx="${idx}"]`)!.classList.remove('tw-invisible');
document.querySelector(`[data-path="${CSS.escape(String(path))}"] .add-code-comment[data-idx="${CSS.escape(String(idx))}"]`)!.classList.remove('tw-invisible');
} else {
document.querySelector(`[data-path="${path}"] .add-code-comment[data-side="${side}"][data-idx="${idx}"]`)!.classList.remove('tw-invisible');
document.querySelector(`[data-path="${CSS.escape(String(path))}"] .add-code-comment[data-side="${CSS.escape(String(side))}"][data-idx="${CSS.escape(String(idx))}"]`)!.classList.remove('tw-invisible');
}
}
conversationHolder.remove();
+2 -2
View File
@@ -120,11 +120,11 @@ function initRepoProjectColumnEdit(writableProjectBoard: Element): void {
}
// update the newly saved column title and color in the project board (to avoid reload)
const elEditButton = writableProjectBoard.querySelector<HTMLButtonElement>(`.show-project-column-modal-edit[${attrDataColumnId}="${columnId}"]`)!;
const elEditButton = writableProjectBoard.querySelector<HTMLButtonElement>(`.show-project-column-modal-edit[${CSS.escape(attrDataColumnId)}="${CSS.escape(columnId)}"]`)!;
elEditButton.setAttribute(attrDataColumnTitle, elColumnTitle.value);
elEditButton.setAttribute(attrDataColumnColor, elColumnColor.value);
const elBoardColumn = writableProjectBoard.querySelector<HTMLElement>(`.project-column[data-id="${columnId}"]`)!;
const elBoardColumn = writableProjectBoard.querySelector<HTMLElement>(`.project-column[data-id="${CSS.escape(columnId)}"]`)!;
const elBoardColumnTitle = elBoardColumn.querySelector<HTMLElement>(`.project-column-title-text`)!;
elBoardColumnTitle.textContent = elColumnTitle.value;
if (elColumnColor.value) {
+1 -1
View File
@@ -12,7 +12,7 @@ export function initRepoReleaseNew() {
registerGlobalEventFunc('click', 'onReleaseEditAttachmentDelete', (el) => {
const uuid = el.getAttribute('data-uuid')!;
const id = el.getAttribute('data-id')!;
document.querySelector<HTMLInputElement>(`input[name='attachment-del-${uuid}']`)!.value = 'true';
document.querySelector<HTMLInputElement>(`input[name='attachment-del-${CSS.escape(uuid)}']`)!.value = 'true';
hideElem(`#attachment-${id}`);
});
registerGlobalInitFunc('initReleaseEditForm', (elForm: HTMLFormElement) => {
+3 -3
View File
@@ -79,7 +79,7 @@ async function loginPasskey() {
}
const reply = await res.json();
window.location.href = reply?.redirect ?? `${appSubUrl}/`;
window.location.assign(reply?.redirect ?? `${appSubUrl}/`);
} catch (err) {
webAuthnError('general', errorMessage(err));
}
@@ -151,7 +151,7 @@ async function verifyAssertion(assertedCredential: any) { // TODO: Credential ty
}
const reply = await res.json();
window.location.href = reply?.redirect ?? `${appSubUrl}/`;
window.location.assign(reply?.redirect ?? `${appSubUrl}/`);
}
async function webauthnRegistered(newCredential: any) { // TODO: Credential type does not work
@@ -188,7 +188,7 @@ function webAuthnError(errorType: ErrorType, message:string = '') {
if (errorType === 'general') {
elErrorMsg.textContent = message || 'unknown error';
} else {
const elTypedError = document.querySelector(`#webauthn-error [data-webauthn-error-msg=${errorType}]`);
const elTypedError = document.querySelector(`#webauthn-error [data-webauthn-error-msg=${CSS.escape(errorType)}]`);
if (elTypedError) {
elErrorMsg.textContent = `${elTypedError.textContent}${message ? ` ${message}` : ''}`;
} else {
+6 -14
View File
@@ -18,15 +18,9 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
const level = parseInt(el.tagName.slice(1));
el.textContent = `${'#'.repeat(level)} ${el.textContent.trim()}`;
},
STRONG(el: HTMLElement) {
return `**${el.textContent}**`;
},
EM(el: HTMLElement) {
return `_${el.textContent}_`;
},
DEL(el: HTMLElement) {
return `~~${el.textContent}~~`;
},
STRONG: (el: HTMLElement) => `**${el.textContent}**`,
EM: (el: HTMLElement) => `_${el.textContent}_`,
DEL: (el: HTMLElement) => `~~${el.textContent}~~`,
A(el: HTMLElement) {
const text = el.textContent || 'link';
const href = el.getAttribute('href');
@@ -62,9 +56,7 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
el.textContent = `${' '.repeat(nestingIdentLevel * 4)}${bullet}${el.textContent}${ctx.elementIsLast ? '' : '\n'}`;
return el;
},
INPUT(el: HTMLElement) {
return (el as HTMLInputElement).checked ? '[x] ' : '[ ] ';
},
INPUT: (el: HTMLElement) => (el as HTMLInputElement).checked ? '[x] ' : '[ ] ',
CODE(el: HTMLElement) {
const text = el.textContent;
if (el.parentNode && (el.parentNode as HTMLElement).tagName === 'PRE') {
@@ -86,8 +78,8 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
function processElement(ctx :ProcessorContext, processors: Processors, el: HTMLElement): string | void {
if (el.hasAttribute('data-markdown-generated-content')) return el.textContent;
if (el.tagName === 'A' && el.children.length === 1 && el.children[0].tagName === 'IMG') {
return processElement(ctx, processors, el.children[0] as HTMLElement);
if (el.tagName === 'A' && el.children.length === 1 && el.firstElementChild!.tagName === 'IMG') {
return processElement(ctx, processors, el.firstElementChild as HTMLElement);
}
const isListContainer = el.tagName === 'OL' || el.tagName === 'UL';
-1
View File
@@ -29,7 +29,6 @@ describe('navigateToIframeLink', () => {
test('unsafe links', () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
window.location.href = 'http://localhost:3000/';
// eslint-disable-next-line no-script-url
navigateToIframeLink('javascript:void(0);', '_blank');
+1 -1
View File
@@ -4,7 +4,7 @@ import {isDarkTheme} from '../utils.ts';
function safeRenderIframeLink(link: any): string | null {
try {
const url = new URL(`${link}`, window.location.href);
const url = new URL(link, window.location.href);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
console.error(`Unsupported link protocol: ${link}`);
return null;
+1 -3
View File
@@ -203,9 +203,7 @@ export async function createCodeEditor(textarea: HTMLTextAreaElement, filenameIn
},
}),
cm.language.foldGutter({
markerDOM(open: boolean) {
return createElementFromHTML(svg(open ? 'octicon-chevron-down' : 'octicon-chevron-right', 13));
},
markerDOM: (open: boolean) => createElementFromHTML(svg(open ? 'octicon-chevron-down' : 'octicon-chevron-right', 13)),
}),
cm.view.highlightActiveLineGutter(),
cm.view.highlightSpecialChars(),
+3 -4
View File
@@ -20,11 +20,11 @@ export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error', d
}
// compact the message to a data attribute to avoid too many duplicated messages
const msgCompact = `${msgType}-${msg.trim()}`.replace(/[^-\w\u{80}-\u{10FFFF}]+/gu, '');
let msgContainer = parentContainer.querySelector<HTMLDivElement>(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
let msgContainer = parentContainer.querySelector<HTMLDivElement>(`.js-global-error[data-global-error-msg-compact="${CSS.escape(msgCompact)}"]`);
if (!msgContainer) {
const el = document.createElement('div');
el.innerHTML = html`<div class="ui container js-global-error tw-my-[--page-spacing]"><details class="ui ${msgType} message"><summary></summary></details></div>`;
msgContainer = el.childNodes[0] as HTMLDivElement;
msgContainer = el.firstElementChild as HTMLDivElement;
}
// merge duplicated messages into "the message (count)" format
@@ -51,8 +51,7 @@ export function isGiteaError(filename: string, stack: string): boolean {
if (extensionRe.test(filename) || extensionRe.test(stack)) return false;
const assetBaseUrl = new URL(`${window.config.assetUrlPrefix}/`, window.location.origin).href;
if (filename && !filename.startsWith(assetBaseUrl) && !filename.startsWith(window.location.origin)) return false;
if (stack && !stack.includes(assetBaseUrl)) return false;
return true;
return !stack || stack.includes(assetBaseUrl);
}
export function processWindowErrorEvent({error, reason, message, type, filename, lineno, colno}: ErrorEvent & PromiseRejectionEvent) {
+5 -5
View File
@@ -253,22 +253,22 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT
dropdown.addEventListener('mousedown', () => {
ignoreClickPreVisible += isMenuVisible() ? 1 : 0;
ignoreClickPreEvents++;
}, true);
}, {capture: true});
dropdown.addEventListener('focus', () => {
ignoreClickPreVisible += isMenuVisible() ? 1 : 0;
ignoreClickPreEvents++;
deferredRefreshAriaActiveItem();
}, true);
}, {capture: true});
dropdown.addEventListener('blur', () => {
ignoreClickPreVisible = ignoreClickPreEvents = 0;
deferredRefreshAriaActiveItem(100);
}, true);
}, {capture: true});
dropdown.addEventListener('mouseup', () => {
setTimeout(() => {
ignoreClickPreVisible = ignoreClickPreEvents = 0;
deferredRefreshAriaActiveItem(100);
}, 0);
}, true);
}, {capture: true});
dropdown.addEventListener('click', (e: MouseEvent) => {
if (isMenuVisible() &&
ignoreClickPreVisible !== 2 && // dropdown is switch from invisible to visible
@@ -277,7 +277,7 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT
e.stopPropagation(); // if the dropdown menu has been opened by focus, do not trigger the next click event again
}
ignoreClickPreEvents = ignoreClickPreVisible = 0;
}, true);
}, {capture: true});
}
// Although Fomantic Dropdown supports "hideDividers", it doesn't really work with our "scoped dividers"
+1 -1
View File
@@ -8,7 +8,7 @@ export function initTabSwitcher(tabItemContainer: Element) {
for (const elItem of tabItems) {
const tabName = elItem.getAttribute('data-tab')!;
elItem.addEventListener('click', () => {
const elPanel = document.querySelector(`.ui.tab[data-tab="${tabName}"]`)!;
const elPanel = document.querySelector(`.ui.tab[data-tab="${CSS.escape(tabName)}"]`)!;
queryElemSiblings(elPanel, '.ui.tab', (el) => el.classList.remove('active'));
queryElemSiblings(elItem, '.item[data-tab]', (el) => el.classList.remove('active'));
elItem.classList.add('active');
+1 -1
View File
@@ -89,7 +89,7 @@ function attachTooltip(target: Element, content: Content | null = null): Instanc
allowHTML: target.getAttribute('data-tooltip-render') === 'html',
placement: target.getAttribute('data-tooltip-placement') as Placement || 'top-start',
followCursor: target.getAttribute('data-tooltip-follow-cursor') as Props['followCursor'] || false,
...(target.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true, aria: {content: 'describedby', expanded: false}} : {}),
...((target.getAttribute('data-tooltip-interactive') === 'true') && {interactive: true, aria: {content: 'describedby', expanded: false}}),
};
if (!target._tippy) {
+1 -1
View File
@@ -45,7 +45,7 @@ type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast};
/** See https://github.com/apvarun/toastify-js#api for options */
function showToast(message: string, level: Intent, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast | null {
const parent = document.querySelector('.ui.dimmer.active') ?? document.body;
const duplicateKey = preventDuplicates ? (preventDuplicates === true ? `${level}-${message}` : preventDuplicates) : '';
const duplicateKey = preventDuplicates ? (typeof preventDuplicates === 'string' ? preventDuplicates : `${level}-${message}`) : '';
// prevent showing duplicate toasts with the same level and message, and give visual feedback for end users
if (preventDuplicates) {
+1 -1
View File
@@ -50,7 +50,7 @@ export class UserEventsSharedWorker {
// * in this case, the logout fetch call already completes and has sent the "logout" message to the worker
// * there can be a data-race between the fetch call's redirection and the "logout" message from the worker
// * the fetch call's logout redirection should always win over the worker message, because it might have a custom location
setTimeout(() => { window.location.href = `${appSubUrl}/` }, 1000);
setTimeout(() => { window.location.assign(`${appSubUrl}/`) }, 1000);
} else if (event.data.type === 'close') {
this.sharedWorker.port.postMessage({type: 'close'});
this.sharedWorker.port.close();
@@ -4,9 +4,7 @@ export function newInplacePluginPdfViewer(): InplaceRenderPlugin {
return {
name: 'pdf-viewer',
canHandle(filename: string, _mimeType: string): boolean {
return filename.toLowerCase().endsWith('.pdf');
},
canHandle: (filename: string, _mimeType: string): boolean => filename.toLowerCase().endsWith('.pdf'),
async render(container: HTMLElement, fileUrl: string): Promise<void> {
const PDFObject = await import('pdfobject');
+2 -2
View File
@@ -9,8 +9,8 @@ type ElementsCallback<T extends Element> = (el: T) => Promisable<any>;
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]): ArrayLikeIterable<Element> {
if (typeof el === 'string' || el instanceof String) {
el = document.querySelectorAll(el as string);
if (typeof el === 'string') {
el = document.querySelectorAll(el);
}
if (el instanceof Node) {
func(el, ...args);
+1 -1
View File
@@ -8,7 +8,7 @@ import type {Issue, Mention} from '../types.ts';
const maxMatches = 6;
function sortAndReduce<T>(map: Map<T, number>): T[] {
const sortedMap = new Map(Array.from(map.entries()).sort((a, b) => a[1] - b[1]));
const sortedMap = new Map(Array.from(map).sort((a, b) => a[1] - b[1]));
return Array.from(sortedMap.keys()).slice(0, maxMatches);
}
+2 -2
View File
@@ -10,11 +10,11 @@ export function dedent(str: string) {
const match = str.match(/^[ \t]*(?=\S)/gm);
if (!match) return str;
let minIndent = Number.POSITIVE_INFINITY;
let minIndent = Infinity;
for (const indent of match) {
minIndent = Math.min(minIndent, indent.length);
}
if (minIndent === 0 || minIndent === Number.POSITIVE_INFINITY) {
if (minIndent === 0 || minIndent === Infinity) {
return str;
}
+1 -1
View File
@@ -37,7 +37,7 @@ export function firstStartDateAfterDate(inputDate: Date): number {
}
const dayOfWeek = inputDate.getUTCDay();
const daysUntilSunday = 7 - dayOfWeek;
const resultDate = new Date(inputDate.getTime());
const resultDate = new Date(inputDate);
resultDate.setUTCDate(resultDate.getUTCDate() + daysUntilSunday);
return resultDate.valueOf();
}
+2 -2
View File
@@ -21,7 +21,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
this.popup.style.display = '';
this.button!.setAttribute('aria-expanded', 'true');
setTimeout(() => this.popup.focus(), 0);
document.addEventListener('click', this.onClickOutside, true);
document.addEventListener('click', this.onClickOutside, {capture: true});
}
hidePopup() {
@@ -125,7 +125,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement {
const itemRight = item.offsetLeft + item.offsetWidth;
if (menuRight - itemRight < 38) { // roughly the width of .overflow-menu-button with some extra space
const onlyLastItem = idx === menuItems.length - 1 && this.overflowItems.length === 0;
const lastItemFit = onlyLastItem && menuRight - itemRight > 0;
const lastItemFit = onlyLastItem && menuRight > itemRight;
const moveToPopup = !onlyLastItem || !lastItemFit;
if (moveToPopup) this.overflowItems.push(item);
}