2023-09-04 09:49:39 +03:00
|
|
|
|
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();
|
|
|
|
|
},
|
2023-09-21 17:31:12 +03:00
|
|
|
|
select: () => {
|
|
|
|
|
searchFieldRef.current.select();
|
|
|
|
|
},
|
2023-09-04 09:49:39 +03:00
|
|
|
|
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"
|
2023-09-22 15:39:05 +03:00
|
|
|
|
autocomplete="off"
|
|
|
|
|
autocorrect="off"
|
|
|
|
|
autocapitalize="off"
|
2024-03-07 11:33:56 +03:00
|
|
|
|
spellCheck="false"
|
2023-09-04 09:49:39 +03:00
|
|
|
|
onSearch={(e) => {
|
|
|
|
|
if (!e.target.value) {
|
|
|
|
|
setSearchParams({});
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
onInput={(e) => {
|
|
|
|
|
setQuery(e.target.value);
|
|
|
|
|
setSearchMenuOpen(true);
|
|
|
|
|
}}
|
|
|
|
|
onFocus={() => {
|
|
|
|
|
setSearchMenuOpen(true);
|
2024-01-29 16:11:08 +03:00
|
|
|
|
formRef.current
|
|
|
|
|
?.querySelector('.search-popover-item')
|
|
|
|
|
?.classList.add('focus');
|
2023-09-04 09:49:39 +03:00
|
|
|
|
}}
|
|
|
|
|
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);
|
2023-09-05 18:30:11 +03:00
|
|
|
|
props?.onSubmit?.(e);
|
2023-09-04 09:49:39 +03:00
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<div class="search-popover" hidden={!searchMenuOpen || !query}>
|
2024-01-29 16:11:08 +03:00
|
|
|
|
{/* {!!query && (
|
|
|
|
|
<Link
|
|
|
|
|
to={`/search?q=${encodeURIComponent(query)}`}
|
|
|
|
|
class="search-popover-item focus"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
props?.onSubmit?.(e);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon icon="search" />
|
|
|
|
|
<span>{query}</span>
|
|
|
|
|
</Link>
|
|
|
|
|
)} */}
|
2023-09-04 09:49:39 +03:00
|
|
|
|
{!!query &&
|
|
|
|
|
[
|
2024-01-29 16:11:08 +03:00
|
|
|
|
{
|
|
|
|
|
label: (
|
|
|
|
|
<>
|
|
|
|
|
{query}{' '}
|
|
|
|
|
<small class="insignificant">
|
|
|
|
|
‒ accounts, hashtags & posts
|
|
|
|
|
</small>
|
|
|
|
|
</>
|
|
|
|
|
),
|
|
|
|
|
to: `/search?q=${encodeURIComponent(query)}`,
|
|
|
|
|
top: !type && !/\s/.test(query),
|
|
|
|
|
hidden: !!type,
|
|
|
|
|
},
|
2023-09-04 09:49:39 +03:00
|
|
|
|
{
|
|
|
|
|
label: (
|
|
|
|
|
<>
|
|
|
|
|
Posts with <q>{query}</q>
|
|
|
|
|
</>
|
|
|
|
|
),
|
|
|
|
|
to: `/search?q=${encodeURIComponent(query)}&type=statuses`,
|
|
|
|
|
hidden: /^https?:/.test(query),
|
2024-01-29 16:11:08 +03:00
|
|
|
|
top: /\s/.test(query),
|
|
|
|
|
icon: 'document',
|
2024-02-23 13:00:30 +03:00
|
|
|
|
queryType: 'statuses',
|
2023-09-04 09:49:39 +03:00
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
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',
|
2024-01-29 16:11:08 +03:00
|
|
|
|
icon: 'hashtag',
|
2024-02-23 13:00:30 +03:00
|
|
|
|
queryType: 'hashtags',
|
2023-09-04 09:49:39 +03:00
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
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`,
|
2024-01-29 16:11:08 +03:00
|
|
|
|
icon: 'group',
|
2024-02-23 13:00:30 +03:00
|
|
|
|
queryType: 'accounts',
|
2023-09-04 09:49:39 +03:00
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
.sort((a, b) => {
|
2024-02-23 13:00:30 +03:00
|
|
|
|
if (type) {
|
|
|
|
|
if (a.queryType === type) return -1;
|
|
|
|
|
if (b.queryType === type) return 1;
|
|
|
|
|
}
|
2023-09-04 09:49:39 +03:00
|
|
|
|
if (a.top && !b.top) return -1;
|
|
|
|
|
if (!a.top && b.top) return 1;
|
|
|
|
|
return 0;
|
|
|
|
|
})
|
2024-01-29 16:11:08 +03:00
|
|
|
|
.filter(({ hidden }) => !hidden)
|
|
|
|
|
.map(({ label, to, icon, type }, i) => (
|
2023-09-04 12:01:06 +03:00
|
|
|
|
<Link
|
|
|
|
|
to={to}
|
2024-01-29 16:11:08 +03:00
|
|
|
|
class={`search-popover-item ${i === 0 ? 'focus' : ''}`}
|
|
|
|
|
// hidden={hidden}
|
2023-09-04 12:01:06 +03:00
|
|
|
|
onClick={(e) => {
|
|
|
|
|
props?.onSubmit?.(e);
|
|
|
|
|
}}
|
|
|
|
|
>
|
2023-09-04 09:49:39 +03:00
|
|
|
|
<Icon
|
2024-01-29 16:11:08 +03:00
|
|
|
|
icon={icon || (type === 'link' ? 'arrow-right' : 'search')}
|
2023-09-04 09:49:39 +03:00
|
|
|
|
class="more-insignificant"
|
|
|
|
|
/>
|
|
|
|
|
<span>{label}</span>{' '}
|
|
|
|
|
</Link>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export default SearchForm;
|