mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-13 03:26:21 +01:00
Global search command trigger
This commit is contained in:
parent
3511ba760a
commit
0fd719d3e7
6 changed files with 376 additions and 219 deletions
|
@ -26,6 +26,7 @@ import Loader from './components/loader';
|
||||||
import MediaModal from './components/media-modal';
|
import MediaModal from './components/media-modal';
|
||||||
import Modal from './components/modal';
|
import Modal from './components/modal';
|
||||||
import NotificationService from './components/notification-service';
|
import NotificationService from './components/notification-service';
|
||||||
|
import SearchCommand from './components/search-command';
|
||||||
import Shortcuts from './components/shortcuts';
|
import Shortcuts from './components/shortcuts';
|
||||||
import ShortcutsSettings from './components/shortcuts-settings';
|
import ShortcutsSettings from './components/shortcuts-settings';
|
||||||
import NotFound from './pages/404';
|
import NotFound from './pages/404';
|
||||||
|
@ -449,6 +450,7 @@ function App() {
|
||||||
)}
|
)}
|
||||||
<NotificationService />
|
<NotificationService />
|
||||||
<BackgroundService isLoggedIn={isLoggedIn} />
|
<BackgroundService isLoggedIn={isLoggedIn} />
|
||||||
|
<SearchCommand onClose={focusDeck} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
54
src/components/search-command.css
Normal file
54
src/components/search-command.css
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
#search-command-container {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1002;
|
||||||
|
background-color: var(--backdrop-darker-color);
|
||||||
|
background-image: radial-gradient(
|
||||||
|
farthest-corner at top,
|
||||||
|
var(--backdrop-color),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 16px;
|
||||||
|
transition: opacity 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
#search-command-container[hidden] {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-command-container form {
|
||||||
|
width: calc(40em - 32px);
|
||||||
|
max-width: 100%;
|
||||||
|
transition: transform 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
#search-command-container[hidden] form {
|
||||||
|
transform: translateY(-64px) scale(0.9);
|
||||||
|
}
|
||||||
|
#search-command-container input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background-color: var(--bg-faded-color);
|
||||||
|
border: 2px solid var(--outline-color);
|
||||||
|
box-shadow: 0 2px 16px var(--drop-shadow-color),
|
||||||
|
0 32px 64px var(--drop-shadow-color);
|
||||||
|
}
|
||||||
|
#search-command-container input:focus {
|
||||||
|
outline: 0;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-color: var(--link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
#search-command-container {
|
||||||
|
align-items: center;
|
||||||
|
background-image: radial-gradient(
|
||||||
|
closest-side,
|
||||||
|
var(--backdrop-color),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
67
src/components/search-command.jsx
Normal file
67
src/components/search-command.jsx
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import './search-command.css';
|
||||||
|
|
||||||
|
import { useRef, useState } from 'preact/hooks';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
|
import SearchForm from './search-form';
|
||||||
|
|
||||||
|
export default function SearchCommand({ onClose = () => {} }) {
|
||||||
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
|
const searchFormRef = useRef(null);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'/',
|
||||||
|
(e) => {
|
||||||
|
setShowSearch(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
searchFormRef.current?.focus?.();
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
ignoreEventWhen: (e) => {
|
||||||
|
const isSearchPage = /\/search/.test(location.hash);
|
||||||
|
const hasModal = !!document.querySelector('#modal-container > *');
|
||||||
|
return isSearchPage || hasModal;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeSearch = () => {
|
||||||
|
setShowSearch(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'esc',
|
||||||
|
(e) => {
|
||||||
|
searchFormRef.current?.blur?.();
|
||||||
|
closeSearch();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: showSearch,
|
||||||
|
enableOnFormTags: true,
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="search-command-container"
|
||||||
|
hidden={!showSearch}
|
||||||
|
onClick={(e) => {
|
||||||
|
console.log(e);
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
closeSearch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SearchForm
|
||||||
|
ref={searchFormRef}
|
||||||
|
onSubmit={() => {
|
||||||
|
closeSearch();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
237
src/components/search-form.jsx
Normal file
237
src/components/search-form.jsx
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
import { forwardRef } from 'preact/compat';
|
||||||
|
import { useImperativeHandle, useRef, useState } from 'preact/hooks';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { api } from '../utils/api';
|
||||||
|
|
||||||
|
import Icon from './icon';
|
||||||
|
import Link from './link';
|
||||||
|
|
||||||
|
const SearchForm = forwardRef((props, ref) => {
|
||||||
|
const { instance } = api();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
|
||||||
|
const [query, setQuery] = useState(searchParams.get('q') || '');
|
||||||
|
const type = searchParams.get('type');
|
||||||
|
const formRef = useRef(null);
|
||||||
|
|
||||||
|
const searchFieldRef = useRef(null);
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
setValue: (value) => {
|
||||||
|
setQuery(value);
|
||||||
|
},
|
||||||
|
focus: () => {
|
||||||
|
searchFieldRef.current.focus();
|
||||||
|
},
|
||||||
|
blur: () => {
|
||||||
|
searchFieldRef.current.blur();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
ref={formRef}
|
||||||
|
class="search-popover-container"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const isSearchPage = /\/search/.test(location.hash);
|
||||||
|
if (isSearchPage) {
|
||||||
|
if (query) {
|
||||||
|
const params = {
|
||||||
|
q: query,
|
||||||
|
};
|
||||||
|
if (type) params.type = type; // Preserve type
|
||||||
|
setSearchParams(params);
|
||||||
|
} else {
|
||||||
|
setSearchParams({});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (query) {
|
||||||
|
location.hash = `/search?q=${encodeURIComponent(query)}${
|
||||||
|
type ? `&type=${type}` : ''
|
||||||
|
}`;
|
||||||
|
} else {
|
||||||
|
location.hash = `/search`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
props?.onSubmit?.(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={searchFieldRef}
|
||||||
|
value={query}
|
||||||
|
name="q"
|
||||||
|
type="search"
|
||||||
|
// autofocus
|
||||||
|
placeholder="Search"
|
||||||
|
dir="auto"
|
||||||
|
onSearch={(e) => {
|
||||||
|
if (!e.target.value) {
|
||||||
|
setSearchParams({});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onInput={(e) => {
|
||||||
|
setQuery(e.target.value);
|
||||||
|
setSearchMenuOpen(true);
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
setSearchMenuOpen(true);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setSearchMenuOpen(false);
|
||||||
|
}, 100);
|
||||||
|
formRef.current
|
||||||
|
?.querySelector('.search-popover-item.focus')
|
||||||
|
?.classList.remove('focus');
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
const { key } = e;
|
||||||
|
switch (key) {
|
||||||
|
case 'Escape':
|
||||||
|
setSearchMenuOpen(false);
|
||||||
|
break;
|
||||||
|
case 'Down':
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault();
|
||||||
|
if (searchMenuOpen) {
|
||||||
|
const focusItem = formRef.current.querySelector(
|
||||||
|
'.search-popover-item.focus',
|
||||||
|
);
|
||||||
|
if (focusItem) {
|
||||||
|
let nextItem = focusItem.nextElementSibling;
|
||||||
|
while (nextItem && nextItem.hidden) {
|
||||||
|
nextItem = nextItem.nextElementSibling;
|
||||||
|
}
|
||||||
|
if (nextItem) {
|
||||||
|
nextItem.classList.add('focus');
|
||||||
|
const siblings = Array.from(
|
||||||
|
nextItem.parentElement.children,
|
||||||
|
).filter((el) => el !== nextItem);
|
||||||
|
siblings.forEach((el) => {
|
||||||
|
el.classList.remove('focus');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const firstItem = formRef.current.querySelector(
|
||||||
|
'.search-popover-item',
|
||||||
|
);
|
||||||
|
if (firstItem) {
|
||||||
|
firstItem.classList.add('focus');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Up':
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault();
|
||||||
|
if (searchMenuOpen) {
|
||||||
|
const focusItem = document.querySelector(
|
||||||
|
'.search-popover-item.focus',
|
||||||
|
);
|
||||||
|
if (focusItem) {
|
||||||
|
let prevItem = focusItem.previousElementSibling;
|
||||||
|
while (prevItem && prevItem.hidden) {
|
||||||
|
prevItem = prevItem.previousElementSibling;
|
||||||
|
}
|
||||||
|
if (prevItem) {
|
||||||
|
prevItem.classList.add('focus');
|
||||||
|
const siblings = Array.from(
|
||||||
|
prevItem.parentElement.children,
|
||||||
|
).filter((el) => el !== prevItem);
|
||||||
|
siblings.forEach((el) => {
|
||||||
|
el.classList.remove('focus');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const lastItem = document.querySelector(
|
||||||
|
'.search-popover-item:last-child',
|
||||||
|
);
|
||||||
|
if (lastItem) {
|
||||||
|
lastItem.classList.add('focus');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
if (searchMenuOpen) {
|
||||||
|
const focusItem = document.querySelector(
|
||||||
|
'.search-popover-item.focus',
|
||||||
|
);
|
||||||
|
if (focusItem) {
|
||||||
|
e.preventDefault();
|
||||||
|
focusItem.click();
|
||||||
|
props?.onSubmit?.(e);
|
||||||
|
}
|
||||||
|
setSearchMenuOpen(false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="search-popover" hidden={!searchMenuOpen || !query}>
|
||||||
|
{!!query &&
|
||||||
|
[
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
Posts with <q>{query}</q>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
to: `/search?q=${encodeURIComponent(query)}&type=statuses`,
|
||||||
|
hidden: /^https?:/.test(query),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
Posts tagged with <mark>#{query.replace(/^#/, '')}</mark>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
to: `/${instance}/t/${query.replace(/^#/, '')}`,
|
||||||
|
hidden:
|
||||||
|
/^@/.test(query) || /^https?:/.test(query) || /\s/.test(query),
|
||||||
|
top: /^#/.test(query),
|
||||||
|
type: 'link',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
Look up <mark>{query}</mark>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
to: `/${query}`,
|
||||||
|
hidden: !/^https?:/.test(query),
|
||||||
|
top: /^https?:/.test(query),
|
||||||
|
type: 'link',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
Accounts with <q>{query}</q>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
to: `/search?q=${encodeURIComponent(query)}&type=accounts`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.top && !b.top) return -1;
|
||||||
|
if (!a.top && b.top) return 1;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
.map(({ label, to, hidden, type }) => (
|
||||||
|
<Link to={to} class="search-popover-item" hidden={hidden}>
|
||||||
|
<Icon
|
||||||
|
icon={type === 'link' ? 'arrow-right' : 'search'}
|
||||||
|
class="more-insignificant"
|
||||||
|
/>
|
||||||
|
<span>{label}</span>{' '}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SearchForm;
|
|
@ -52,6 +52,7 @@
|
||||||
--outline-hover-color: rgba(128, 128, 128, 0.7);
|
--outline-hover-color: rgba(128, 128, 128, 0.7);
|
||||||
--divider-color: rgba(0, 0, 0, 0.1);
|
--divider-color: rgba(0, 0, 0, 0.1);
|
||||||
--backdrop-color: rgba(0, 0, 0, 0.05);
|
--backdrop-color: rgba(0, 0, 0, 0.05);
|
||||||
|
--backdrop-darker-color: rgba(0, 0, 0, 0.25);
|
||||||
--backdrop-solid-color: #ccc;
|
--backdrop-solid-color: #ccc;
|
||||||
--img-bg-color: rgba(128, 128, 128, 0.2);
|
--img-bg-color: rgba(128, 128, 128, 0.2);
|
||||||
--loader-color: #1c1e2199;
|
--loader-color: #1c1e2199;
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
import './search.css';
|
import './search.css';
|
||||||
|
|
||||||
import { forwardRef } from 'preact/compat';
|
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks';
|
||||||
import {
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
|
||||||
useLayoutEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'preact/hooks';
|
|
||||||
import { InView } from 'react-intersection-observer';
|
import { InView } from 'react-intersection-observer';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -16,6 +10,7 @@ import Icon from '../components/icon';
|
||||||
import Link from '../components/link';
|
import Link from '../components/link';
|
||||||
import Loader from '../components/loader';
|
import Loader from '../components/loader';
|
||||||
import NavMenu from '../components/nav-menu';
|
import NavMenu from '../components/nav-menu';
|
||||||
|
import SearchForm from '../components/search-form';
|
||||||
import Status from '../components/status';
|
import Status from '../components/status';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
@ -128,10 +123,21 @@ function Search(props) {
|
||||||
if (q) {
|
if (q) {
|
||||||
searchFormRef.current?.setValue?.(q);
|
searchFormRef.current?.setValue?.(q);
|
||||||
loadResults(true);
|
loadResults(true);
|
||||||
}
|
} else {
|
||||||
searchFormRef.current?.focus?.();
|
searchFormRef.current?.focus?.();
|
||||||
|
}
|
||||||
}, [q, type, instance]);
|
}, [q, type, instance]);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'/',
|
||||||
|
(e) => {
|
||||||
|
searchFormRef.current?.focus?.();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="search-page" class="deck-container" ref={scrollableRef}>
|
<div id="search-page" class="deck-container" ref={scrollableRef}>
|
||||||
<div class="timeline-deck deck">
|
<div class="timeline-deck deck">
|
||||||
|
@ -356,213 +362,3 @@ function Search(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Search;
|
export default Search;
|
||||||
|
|
||||||
const SearchForm = forwardRef((props, ref) => {
|
|
||||||
const { instance } = api();
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
|
|
||||||
const [query, setQuery] = useState(searchParams.get('q') || '');
|
|
||||||
const type = searchParams.get('type');
|
|
||||||
const formRef = useRef(null);
|
|
||||||
|
|
||||||
const searchFieldRef = useRef(null);
|
|
||||||
useImperativeHandle(ref, () => ({
|
|
||||||
setValue: (value) => {
|
|
||||||
setQuery(value);
|
|
||||||
},
|
|
||||||
focus: () => {
|
|
||||||
searchFieldRef.current.focus();
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
ref={formRef}
|
|
||||||
class="search-popover-container"
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
if (query) {
|
|
||||||
const params = {
|
|
||||||
q: query,
|
|
||||||
};
|
|
||||||
if (type) params.type = type; // Preserve type
|
|
||||||
setSearchParams(params);
|
|
||||||
} else {
|
|
||||||
setSearchParams({});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
ref={searchFieldRef}
|
|
||||||
value={query}
|
|
||||||
name="q"
|
|
||||||
type="search"
|
|
||||||
// autofocus
|
|
||||||
placeholder="Search"
|
|
||||||
dir="auto"
|
|
||||||
onSearch={(e) => {
|
|
||||||
if (!e.target.value) {
|
|
||||||
setSearchParams({});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onInput={(e) => {
|
|
||||||
setQuery(e.target.value);
|
|
||||||
setSearchMenuOpen(true);
|
|
||||||
}}
|
|
||||||
onFocus={() => {
|
|
||||||
setSearchMenuOpen(true);
|
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
setSearchMenuOpen(false);
|
|
||||||
}, 100);
|
|
||||||
formRef.current
|
|
||||||
?.querySelector('.search-popover-item.focus')
|
|
||||||
?.classList.remove('focus');
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
const { key } = e;
|
|
||||||
switch (key) {
|
|
||||||
case 'Escape':
|
|
||||||
setSearchMenuOpen(false);
|
|
||||||
break;
|
|
||||||
case 'Down':
|
|
||||||
case 'ArrowDown':
|
|
||||||
e.preventDefault();
|
|
||||||
if (searchMenuOpen) {
|
|
||||||
const focusItem = formRef.current.querySelector(
|
|
||||||
'.search-popover-item.focus',
|
|
||||||
);
|
|
||||||
if (focusItem) {
|
|
||||||
let nextItem = focusItem.nextElementSibling;
|
|
||||||
while (nextItem && nextItem.hidden) {
|
|
||||||
nextItem = nextItem.nextElementSibling;
|
|
||||||
}
|
|
||||||
if (nextItem) {
|
|
||||||
nextItem.classList.add('focus');
|
|
||||||
const siblings = Array.from(
|
|
||||||
nextItem.parentElement.children,
|
|
||||||
).filter((el) => el !== nextItem);
|
|
||||||
siblings.forEach((el) => {
|
|
||||||
el.classList.remove('focus');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const firstItem = formRef.current.querySelector(
|
|
||||||
'.search-popover-item',
|
|
||||||
);
|
|
||||||
if (firstItem) {
|
|
||||||
firstItem.classList.add('focus');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'Up':
|
|
||||||
case 'ArrowUp':
|
|
||||||
e.preventDefault();
|
|
||||||
if (searchMenuOpen) {
|
|
||||||
const focusItem = document.querySelector(
|
|
||||||
'.search-popover-item.focus',
|
|
||||||
);
|
|
||||||
if (focusItem) {
|
|
||||||
let prevItem = focusItem.previousElementSibling;
|
|
||||||
while (prevItem && prevItem.hidden) {
|
|
||||||
prevItem = prevItem.previousElementSibling;
|
|
||||||
}
|
|
||||||
if (prevItem) {
|
|
||||||
prevItem.classList.add('focus');
|
|
||||||
const siblings = Array.from(
|
|
||||||
prevItem.parentElement.children,
|
|
||||||
).filter((el) => el !== prevItem);
|
|
||||||
siblings.forEach((el) => {
|
|
||||||
el.classList.remove('focus');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const lastItem = document.querySelector(
|
|
||||||
'.search-popover-item:last-child',
|
|
||||||
);
|
|
||||||
if (lastItem) {
|
|
||||||
lastItem.classList.add('focus');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'Enter':
|
|
||||||
if (searchMenuOpen) {
|
|
||||||
const focusItem = document.querySelector(
|
|
||||||
'.search-popover-item.focus',
|
|
||||||
);
|
|
||||||
if (focusItem) {
|
|
||||||
e.preventDefault();
|
|
||||||
focusItem.click();
|
|
||||||
}
|
|
||||||
setSearchMenuOpen(false);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div class="search-popover" hidden={!searchMenuOpen || !query}>
|
|
||||||
{!!query &&
|
|
||||||
[
|
|
||||||
{
|
|
||||||
label: (
|
|
||||||
<>
|
|
||||||
Posts with <q>{query}</q>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
to: `/search?q=${encodeURIComponent(query)}&type=statuses`,
|
|
||||||
hidden: /^https?:/.test(query),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: (
|
|
||||||
<>
|
|
||||||
Posts tagged with <mark>#{query.replace(/^#/, '')}</mark>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
to: `/${instance}/t/${query.replace(/^#/, '')}`,
|
|
||||||
hidden:
|
|
||||||
/^@/.test(query) || /^https?:/.test(query) || /\s/.test(query),
|
|
||||||
top: /^#/.test(query),
|
|
||||||
type: 'link',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: (
|
|
||||||
<>
|
|
||||||
Look up <mark>{query}</mark>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
to: `/${query}`,
|
|
||||||
hidden: !/^https?:/.test(query),
|
|
||||||
top: /^https?:/.test(query),
|
|
||||||
type: 'link',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: (
|
|
||||||
<>
|
|
||||||
Accounts with <q>{query}</q>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
to: `/search?q=${encodeURIComponent(query)}&type=accounts`,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (a.top && !b.top) return -1;
|
|
||||||
if (!a.top && b.top) return 1;
|
|
||||||
return 0;
|
|
||||||
})
|
|
||||||
.map(({ label, to, hidden, type }) => (
|
|
||||||
<Link to={to} class="search-popover-item" hidden={hidden}>
|
|
||||||
<Icon
|
|
||||||
icon={type === 'link' ? 'arrow-right' : 'search'}
|
|
||||||
class="more-insignificant"
|
|
||||||
/>
|
|
||||||
<span>{label}</span>{' '}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
Loading…
Reference in a new issue