|
1 | 1 | import { getCurrentUrl, Link } from 'preact-router' |
2 | | -import { useCallback, useMemo, useRef, useState } from 'preact/hooks' |
| 2 | +import { useCallback } from 'preact/hooks' |
3 | 3 | import type { ConfigGenerator } from '../Config.js' |
4 | 4 | import config from '../Config.js' |
5 | 5 | import { useLocale, useTheme, useTitle, useVersion } from '../contexts/index.js' |
6 | | -import { useFocus } from '../hooks/useFocus.js' |
7 | 6 | import { cleanUrl, getGenerator, SOURCE_REPO_URL } from '../Utils.js' |
| 7 | +import { FancyMenu } from './FancyMenu.jsx' |
8 | 8 | import { Btn, BtnMenu, Icons, Octicon } from './index.js' |
9 | 9 |
|
10 | 10 | const Themes: Record<string, keyof typeof Octicon> = { |
@@ -63,83 +63,40 @@ function GeneratorTitle({ title, gen }: GeneratorTitleProps) { |
63 | 63 | const { locale } = useLocale() |
64 | 64 | const { version } = useVersion() |
65 | 65 |
|
66 | | - const [active, setActive] = useFocus() |
67 | | - const [search, setSearch] = useState('') |
68 | | - const inputRef = useRef<HTMLInputElement>(null) |
69 | | - const resultsRef = useRef<HTMLDivElement>(null) |
70 | | - |
71 | 66 | const icon = Object.keys(Icons).includes(gen.id) ? gen.id as keyof typeof Icons : undefined |
72 | 67 |
|
73 | | - const generators = useMemo(() => { |
74 | | - let result = config.generators |
| 68 | + const getGenerators = useCallback((search: string, close: () => void) => { |
| 69 | + let results = config.generators |
75 | 70 | .filter(g => !g.dependency) |
76 | 71 | .map(g => ({ ...g, name: locale(`generator.${g.id}`).toLowerCase() })) |
77 | 72 | if (search) { |
78 | 73 | const parts = search.split(' ') |
79 | | - result = result.filter(g => parts.some(p => g.name.includes(p)) |
| 74 | + results = results.filter(g => parts.some(p => g.name.includes(p)) |
80 | 75 | || parts.some(p => g.tags?.some(t => t.includes(p)) ?? false)) |
81 | 76 | } |
82 | | - result.sort((a, b) => a.name.localeCompare(b.name)) |
| 77 | + results.sort((a, b) => a.name.localeCompare(b.name)) |
83 | 78 | if (search) { |
84 | | - result.sort((a, b) => (b.name.startsWith(search) ? 1 : 0) - (a.name.startsWith(search) ? 1 : 0)) |
| 79 | + results.sort((a, b) => (b.name.startsWith(search) ? 1 : 0) - (a.name.startsWith(search) ? 1 : 0)) |
85 | 80 | } |
86 | | - return result |
87 | | - }, [locale, version, search]) |
88 | | - |
89 | | - const open = useCallback(() => { |
90 | | - setActive(true) |
91 | | - setTimeout(() => { |
92 | | - inputRef.current?.select() |
93 | | - }) |
94 | | - }, [setActive, inputRef]) |
95 | | - |
96 | | - const handleKeyDown = useCallback((e: KeyboardEvent) => { |
97 | | - if (e.key == 'Enter') { |
98 | | - if (document.activeElement == inputRef.current) { |
99 | | - const firstResult = resultsRef.current?.firstElementChild |
100 | | - if (firstResult instanceof HTMLElement) { |
101 | | - firstResult.click() |
102 | | - } |
103 | | - } |
104 | | - } else if (e.key == 'ArrowDown') { |
105 | | - const nextElement = document.activeElement == inputRef.current |
106 | | - ? resultsRef.current?.firstElementChild |
107 | | - : document.activeElement?.nextElementSibling |
108 | | - if (nextElement instanceof HTMLElement) { |
109 | | - nextElement.focus() |
110 | | - } |
111 | | - e.preventDefault() |
112 | | - } else if (e.key == 'ArrowUp') { |
113 | | - const prevElement = document.activeElement?.previousElementSibling |
114 | | - if (prevElement instanceof HTMLElement) { |
115 | | - prevElement.focus() |
116 | | - } |
117 | | - e.preventDefault() |
118 | | - } else if (e.key == 'Escape') { |
119 | | - setActive(false) |
| 81 | + if (results.length === 0) { |
| 82 | + return [<span class="note">{locale('generators.no_results')}</span>] |
120 | 83 | } |
121 | | - }, [setActive, inputRef]) |
| 84 | + return results.map(g => |
| 85 | + <Link class="gen-result flex items-center cursor-pointer no-underline rounded p-1" href={cleanUrl(g.url)} onClick={close}> |
| 86 | + {locale(`generator.${g.id}`)} |
| 87 | + {Object.keys(Icons).includes(g.id) ? Icons[g.id as keyof typeof Icons] : undefined} |
| 88 | + <div class="m-auto"></div> |
| 89 | + {g.tags?.filter(t => t === 'assets').map(t => |
| 90 | + <div class="badge ml-2 mr-0 text-sm" style="--color: #555;">{t}</div> |
| 91 | + )} |
| 92 | + </Link> |
| 93 | + ) |
| 94 | + }, [locale, version]) |
122 | 95 |
|
123 | | - return <div class="px-1 relative"> |
124 | | - <h1 class="font-bold flex items-center cursor-pointer text-lg sm:text-2xl" onClick={open}> |
| 96 | + return <FancyMenu getResults={getGenerators} placeholder={locale('generators.search')}> |
| 97 | + <h1 class="font-bold flex items-center cursor-pointer text-lg sm:text-2xl"> |
125 | 98 | {title} |
126 | 99 | {icon && Icons[icon]} |
127 | 100 | </h1> |
128 | | - <div class={`gen-menu absolute flex flex-col gap-2 p-2 rounded-lg drop-shadow-xl ${active ? '' : 'hidden'}`} onKeyDown={handleKeyDown}> |
129 | | - <input ref={inputRef} type="text" class="py-1 px-2 w-full rounded" value={search} placeholder={locale('generators.search')} onInput={(e) => setSearch((e.target as HTMLInputElement).value)} onClick={(e) => e.stopPropagation()} /> |
130 | | - {active && <div ref={resultsRef} class="gen-results overflow-y-auto overscroll-none flex flex-col pr-2 h-96 max-h-max min-w-max"> |
131 | | - {generators.length === 0 && <span class="note">{locale('generators.no_results')}</span>} |
132 | | - {generators.map(g => |
133 | | - <Link class="flex items-center cursor-pointer no-underline rounded p-1" href={cleanUrl(g.url)} onClick={() => setActive(false)}> |
134 | | - {locale(`generator.${g.id}`)} |
135 | | - {Object.keys(Icons).includes(g.id) ? Icons[g.id as keyof typeof Icons] : undefined} |
136 | | - <div class="m-auto"></div> |
137 | | - {g.tags?.filter(t => t === 'assets').map(t => |
138 | | - <div class="badge ml-2 mr-0 text-sm" style="--color: #555;">{t}</div> |
139 | | - )} |
140 | | - </Link> |
141 | | - )} |
142 | | - </div>} |
143 | | - </div> |
144 | | - </div> |
| 101 | + </FancyMenu> |
145 | 102 | } |
0 commit comments