chore: fix violations from the newly-enabled unicorn rules

Apply the code changes required by the rules enabled in the previous
commit: String() coercions over template literals, location.assign over
href assignment, URL#href over toString, Object.entries/values loop
forms, and related autofixes. Disable unicorn/no-non-function-verb-prefix,
and disable unicorn/no-error-property-assignment for test files.

Assisted-by: claude-code:opus-4.8
This commit is contained in:
silverwind
2026-06-17 07:00:36 +02:00
parent 668d95a602
commit 767ed117e2
41 changed files with 81 additions and 103 deletions
+2 -1
View File
@@ -812,7 +812,7 @@ export default defineConfig([
'unicorn/no-nested-ternary': [0],
'unicorn/no-new-array': [0],
'unicorn/no-new-buffer': [0],
'unicorn/no-non-function-verb-prefix': [2],
'unicorn/no-non-function-verb-prefix': [0],
'unicorn/no-null': [0],
'unicorn/no-object-as-default-parameter': [0],
'unicorn/no-object-methods-with-collections': [2],
@@ -1053,6 +1053,7 @@ export default defineConfig([
languageOptions: {globals: globals.vitest},
rules: {
'gitea/unescaped-html-literal': [0],
'unicorn/no-error-property-assignment': [0],
'vitest/consistent-test-filename': [0],
'vitest/consistent-test-it': [0],
'vitest/expect-expect': [0],
+1 -1
View File
@@ -90,7 +90,7 @@ export default {
'8xl': '96px',
'9xl': '128px',
...Object.fromEntries(Array.from({length: 100}, (_, i) => {
return [`${i}`, `${i === 0 ? '0' : `${i}px`}`];
return [String(i), i === 0 ? '0' : `${i}px`];
})),
},
extend: {
+1 -1
View File
@@ -83,7 +83,7 @@ async function setPrLabels(): Promise<void> {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${env.GITHUB_TOKEN}`,
'X-GitHub-Api-Version': '2022-11-28',
...(body ? {'Content-Type': 'application/json'} : {}),
...(body && {'Content-Type': 'application/json'}),
},
body: body ? JSON.stringify(body) : undefined,
});
+19 -21
View File
@@ -12,30 +12,28 @@ const rule: JSRuleDefinition<JSRuleDefinitionTypeOptions> = {
},
},
create(context) {
return {
Literal(node) {
if (typeof node.value !== 'string' || !htmlOpenTag.test(node.value)) return;
create: (context) => ({
Literal(node) {
if (typeof node.value !== 'string' || !htmlOpenTag.test(node.value)) return;
context.report({
node,
messageId: 'unescapedHtmlLiteral',
});
},
TemplateLiteral(node) {
const templateStart = node.quasis[0]?.value.raw;
if (!templateStart || !htmlOpenTag.test(templateStart)) return;
context.report({
node,
messageId: 'unescapedHtmlLiteral',
});
},
TemplateLiteral(node) {
const templateStart = node.quasis[0]?.value.raw;
if (!templateStart || !htmlOpenTag.test(templateStart)) return;
const parent = node.parent;
if (parent?.type === 'TaggedTemplateExpression' && parent.tag.type === 'Identifier' && parent.tag.name === 'html') return;
const parent = node.parent;
if (parent?.type === 'TaggedTemplateExpression' && parent.tag.type === 'Identifier' && parent.tag.name === 'html') return;
context.report({
node,
messageId: 'unescapedHtmlLiteral',
});
},
};
},
context.report({
node,
messageId: 'unescapedHtmlLiteral',
});
},
}),
};
export default rule;
+1 -1
View File
@@ -7,7 +7,7 @@ import {argv, exit} from 'node:process';
async function generate(svg: string, path: string, {size, bg}: {size: number, bg?: boolean}) {
const outputFile = new URL(path, import.meta.url);
if (String(outputFile).endsWith('.svg')) {
if (outputFile.href.endsWith('.svg')) {
const {data} = optimize(svg, {
plugins: [
'preset-default',
+1 -1
View File
@@ -92,7 +92,7 @@ async function processMaterialFileIcons() {
}
// Use VSCode's "Language ID" mapping from its extensions
for (const [_, langIdExtMap] of Object.entries(vscodeExtensions)) {
for (const langIdExtMap of Object.values(vscodeExtensions)) {
for (const [langId, names] of Object.entries(langIdExtMap)) {
for (const name of names) {
const nameLower = name.toLowerCase();
+1 -1
View File
@@ -56,7 +56,7 @@ const commonRolldownOptions: Rolldown.RolldownOptions = {
checks: {
pluginTimings: false,
},
...(env.CI ? {plugins: [failOnWarningsPlugin()]} : {}),
...(env.CI && {plugins: [failOnWarningsPlugin()]}),
};
function commonViteOpts({build, ...other}: InlineConfig): InlineConfig {
+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];
+1 -1
View File
@@ -178,7 +178,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"
+4 -4
View File
@@ -24,7 +24,7 @@ export function initGlobalDeleteButton(): void {
btn.addEventListener('click', (e) => {
e.preventDefault();
// `dataset` is used here because the code below depends on it
// eslint-disable-next-line unicorn/dom-node-dataset -- code depends on the camel-casing
const dataObj = btn.dataset;
const modalId = btn.getAttribute('data-modal-id');
@@ -70,7 +70,7 @@ export function initGlobalDeleteButton(): void {
const response = await POST(btn.getAttribute('data-url')!, {data: postData});
if (response.ok) {
const data = await response.json();
window.location.href = data.redirect;
window.location.assign(data.redirect);
}
})();
modal.classList.add('is-loading'); // the request is in progress, so also add loading indicator to the modal
@@ -158,8 +158,8 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
const attrTarget = elModal.querySelector(`#${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;
+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});
+1 -1
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}`);
});
}
+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);
+2 -5
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)));
},
});
+1 -1
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) => {
+2 -2
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
+4 -12
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') {
-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(),
+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
@@ -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
@@ -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');
+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;
}
+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);
}