fix: clear codacy high severity batches
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useRef, useCallback, useMemo, useEffect, type ComponentType, type SVGAttributes } from 'react'
|
||||
import type { VaultEntry } from '../types'
|
||||
import { getTypeColor, getTypeLightColor } from '../utils/typeColors'
|
||||
import { scrollSelectedHTMLChildIntoView } from '../utils/domScroll'
|
||||
import { getTypeIcon } from './NoteItem'
|
||||
import { NoteTitleIcon } from './NoteTitleIcon'
|
||||
import './WikilinkSuggestionMenu.css'
|
||||
@@ -8,6 +9,8 @@ import './WikilinkSuggestionMenu.css'
|
||||
const MIN_QUERY_LENGTH = 2
|
||||
const MAX_RESULTS = 10
|
||||
|
||||
type AutocompleteKeyAction = 'next' | 'previous' | 'select' | 'close'
|
||||
|
||||
interface NoteAutocompleteProps {
|
||||
entries: VaultEntry[]
|
||||
typeEntryMap: Record<string, VaultEntry>
|
||||
@@ -29,26 +32,106 @@ interface MatchedEntry {
|
||||
TypeIcon?: ComponentType<SVGAttributes<SVGSVGElement>>
|
||||
}
|
||||
|
||||
function entryMatchesQuery(entry: VaultEntry, lowerQuery: string): boolean {
|
||||
return entry.title.toLowerCase().includes(lowerQuery)
|
||||
|| entry.aliases.some(alias => alias.toLowerCase().includes(lowerQuery))
|
||||
}
|
||||
|
||||
function buildMatchedEntry(entry: VaultEntry, typeEntryMap: Record<string, VaultEntry>): MatchedEntry {
|
||||
const isA = entry.isA
|
||||
const typeEntry = typeEntryMap[isA ?? '']
|
||||
const noteType = isA || undefined
|
||||
return {
|
||||
title: entry.title,
|
||||
noteIcon: entry.icon,
|
||||
noteType,
|
||||
typeColor: noteType ? getTypeColor(isA, typeEntry?.color) : undefined,
|
||||
typeLightColor: noteType ? getTypeLightColor(isA, typeEntry?.color) : undefined,
|
||||
TypeIcon: noteType ? getTypeIcon(isA, typeEntry?.icon) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function matchEntries(entries: VaultEntry[], typeEntryMap: Record<string, VaultEntry>, query: string): MatchedEntry[] {
|
||||
if (query.length < MIN_QUERY_LENGTH) return []
|
||||
const lowerQuery = query.toLowerCase()
|
||||
const matches = entries.filter(e =>
|
||||
e.title.toLowerCase().includes(lowerQuery) ||
|
||||
e.aliases.some(a => a.toLowerCase().includes(lowerQuery)),
|
||||
return entries
|
||||
.filter(entry => entryMatchesQuery(entry, lowerQuery))
|
||||
.slice(0, MAX_RESULTS)
|
||||
.map(entry => buildMatchedEntry(entry, typeEntryMap))
|
||||
}
|
||||
|
||||
function resolveOpenAutocompleteKeyAction(key: string): AutocompleteKeyAction | null {
|
||||
switch (key) {
|
||||
case 'ArrowDown':
|
||||
return 'next'
|
||||
case 'ArrowUp':
|
||||
return 'previous'
|
||||
case 'Enter':
|
||||
return 'select'
|
||||
case 'Escape':
|
||||
return 'close'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function nextAutocompleteSelectionIndex(currentIndex: number, matchCount: number): number {
|
||||
return (currentIndex + 1) % matchCount
|
||||
}
|
||||
|
||||
function previousAutocompleteSelectionIndex(currentIndex: number, matchCount: number): number {
|
||||
return currentIndex <= 0 ? matchCount - 1 : currentIndex - 1
|
||||
}
|
||||
|
||||
function handleClosedAutocompleteKey(
|
||||
key: string,
|
||||
value: string,
|
||||
onSelect: (noteTitle: string) => void,
|
||||
onEscape: (() => void) | undefined,
|
||||
): void {
|
||||
if (key === 'Enter') {
|
||||
onSelect(value)
|
||||
return
|
||||
}
|
||||
if (key === 'Escape') onEscape?.()
|
||||
}
|
||||
|
||||
function preventAutocompleteMouseDown(event: React.MouseEvent): void {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
interface NoteAutocompleteMenuItemProps {
|
||||
item: MatchedEntry
|
||||
selected: boolean
|
||||
onSelect: (title: string) => void
|
||||
onHover: () => void
|
||||
}
|
||||
|
||||
function NoteAutocompleteMenuItem({
|
||||
item,
|
||||
selected,
|
||||
onSelect,
|
||||
onHover,
|
||||
}: NoteAutocompleteMenuItemProps) {
|
||||
return (
|
||||
<div
|
||||
className={`wikilink-menu__item${selected ? ' wikilink-menu__item--selected' : ''}`}
|
||||
onMouseDown={preventAutocompleteMouseDown}
|
||||
onClick={() => onSelect(item.title)}
|
||||
onMouseEnter={onHover}
|
||||
>
|
||||
<span className="wikilink-menu__title" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{item.TypeIcon && <item.TypeIcon width={14} height={14} style={{ color: item.typeColor, flexShrink: 0 }} />}
|
||||
<NoteTitleIcon icon={item.noteIcon} size={14} />
|
||||
{item.title}
|
||||
</span>
|
||||
{item.noteType && (
|
||||
<span className="wikilink-menu__type" style={{ color: item.typeColor, backgroundColor: item.typeLightColor, borderRadius: 9999, padding: '1px 8px' }}>
|
||||
{item.noteType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
return matches.slice(0, MAX_RESULTS).map(e => {
|
||||
const isA = e.isA
|
||||
const te = typeEntryMap[isA ?? '']
|
||||
const noteType = isA || undefined
|
||||
return {
|
||||
title: e.title,
|
||||
noteIcon: e.icon,
|
||||
noteType,
|
||||
typeColor: noteType ? getTypeColor(isA, te?.color) : undefined,
|
||||
typeLightColor: noteType ? getTypeLightColor(isA, te?.color) : undefined,
|
||||
TypeIcon: noteType ? getTypeIcon(isA, te?.icon) : undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function NoteAutocomplete({ entries, typeEntryMap, value, onChange, onSelect, onEscape, placeholder, autoFocus, testId }: NoteAutocompleteProps) {
|
||||
@@ -65,8 +148,7 @@ export function NoteAutocomplete({ entries, typeEntryMap, value, onChange, onSel
|
||||
// Scroll selected item into view
|
||||
useEffect(() => {
|
||||
if (selectedIndex < 0 || !menuRef.current) return
|
||||
const el = menuRef.current.children[selectedIndex] as HTMLElement | undefined
|
||||
el?.scrollIntoView?.({ block: 'nearest' })
|
||||
scrollSelectedHTMLChildIntoView(menuRef.current, selectedIndex)
|
||||
}, [selectedIndex])
|
||||
|
||||
// Close on outside click
|
||||
@@ -95,28 +177,31 @@ export function NoteAutocomplete({ entries, typeEntryMap, value, onChange, onSel
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (!open || matches.length === 0) {
|
||||
if (e.key === 'Enter') { onSelect(value); return }
|
||||
if (e.key === 'Escape') { onEscape?.(); return }
|
||||
handleClosedAutocompleteKey(e.key, value, onSelect, onEscape)
|
||||
return
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
setSelectedIndex(i => (i + 1) % matches.length)
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
setSelectedIndex(i => (i <= 0 ? matches.length - 1 : i - 1))
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
if (selectedIndex >= 0 && selectedIndex < matches.length) {
|
||||
handleSelect(matches[selectedIndex].title)
|
||||
} else {
|
||||
onSelect(value)
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
|
||||
const action = resolveOpenAutocompleteKeyAction(e.key)
|
||||
if (!action) return
|
||||
|
||||
e.preventDefault()
|
||||
if (action === 'next') {
|
||||
setSelectedIndex(i => nextAutocompleteSelectionIndex(i, matches.length))
|
||||
return
|
||||
}
|
||||
if (action === 'previous') {
|
||||
setSelectedIndex(i => previousAutocompleteSelectionIndex(i, matches.length))
|
||||
return
|
||||
}
|
||||
if (action === 'close') {
|
||||
setOpen(false)
|
||||
onEscape?.()
|
||||
return
|
||||
}
|
||||
|
||||
const match = matches[selectedIndex]
|
||||
if (match) handleSelect(match.title)
|
||||
else onSelect(value)
|
||||
}, [open, matches, selectedIndex, value, handleSelect, onSelect, onEscape])
|
||||
|
||||
return (
|
||||
@@ -136,24 +221,13 @@ export function NoteAutocomplete({ entries, typeEntryMap, value, onChange, onSel
|
||||
{open && matches.length > 0 && (
|
||||
<div className="wikilink-menu" ref={menuRef} style={{ position: 'absolute', top: '100%', left: 0, right: 0, marginTop: 2, minWidth: 'auto' }}>
|
||||
{matches.map((item, index) => (
|
||||
<div
|
||||
<NoteAutocompleteMenuItem
|
||||
key={item.title}
|
||||
className={`wikilink-menu__item${index === selectedIndex ? ' wikilink-menu__item--selected' : ''}`}
|
||||
onMouseDown={e => e.preventDefault()}
|
||||
onClick={() => handleSelect(item.title)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
>
|
||||
<span className="wikilink-menu__title" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
{item.TypeIcon && <item.TypeIcon width={14} height={14} style={{ color: item.typeColor, flexShrink: 0 }} />}
|
||||
<NoteTitleIcon icon={item.noteIcon} size={14} />
|
||||
{item.title}
|
||||
</span>
|
||||
{item.noteType && (
|
||||
<span className="wikilink-menu__type" style={{ color: item.typeColor, backgroundColor: item.typeLightColor, borderRadius: 9999, padding: '1px 8px' }}>
|
||||
{item.noteType}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
item={item}
|
||||
selected={index === selectedIndex}
|
||||
onSelect={handleSelect}
|
||||
onHover={() => setSelectedIndex(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user