feat: add autocomplete tags input and dropdown
This commit is contained in:
91
app/assets/javascripts/components/AutocompleteTagInput.tsx
Normal file
91
app/assets/javascripts/components/AutocompleteTagInput.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { SNTag } from '@standardnotes/snjs';
|
||||
import { FunctionalComponent } from 'preact';
|
||||
import { useRef, useState } from 'preact/hooks';
|
||||
import { Icon } from './Icon';
|
||||
import { Disclosure, DisclosurePanel } from '@reach/disclosure';
|
||||
import { useCloseOnBlur } from './utils';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
};
|
||||
|
||||
export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
||||
application,
|
||||
}) => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [tagResults, setTagResults] = useState<SNTag[]>(() => {
|
||||
return application.searchTags('');
|
||||
});
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||
|
||||
const dropdownRef = useRef<HTMLDivElement>();
|
||||
const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) =>
|
||||
setDropdownVisible(visible)
|
||||
);
|
||||
|
||||
const onSearchQueryChange = (event: Event) => {
|
||||
const query = (event.target as HTMLInputElement).value;
|
||||
const tags = application.searchTags(query);
|
||||
|
||||
setSearchQuery(query);
|
||||
setTagResults(tags);
|
||||
setDropdownVisible(tags.length > 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={(event) => event.preventDefault()} className="mt-2">
|
||||
<Disclosure
|
||||
open={dropdownVisible}
|
||||
onChange={() => setDropdownVisible(true)}
|
||||
>
|
||||
<input
|
||||
className="min-w-80 text-xs no-border h-7 focus:outline-none focus:shadow-none focus:border-bottom"
|
||||
value={searchQuery}
|
||||
onChange={onSearchQueryChange}
|
||||
type="text"
|
||||
onBlur={closeOnBlur}
|
||||
onFocus={() => {
|
||||
if (tagResults.length > 0) {
|
||||
setDropdownVisible(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{dropdownVisible && (
|
||||
<DisclosurePanel
|
||||
ref={dropdownRef}
|
||||
className="sn-dropdown flex flex-col py-2 max-h-120 overflow-y-scroll absolute"
|
||||
>
|
||||
{tagResults.map((tag) => {
|
||||
return (
|
||||
<button
|
||||
key={tag.uuid}
|
||||
className={`flex items-center border-0 focus:inner-ring-info cursor-pointer
|
||||
hover:bg-contrast color-text bg-transparent px-3 text-left py-1.5`}
|
||||
onBlur={closeOnBlur}
|
||||
>
|
||||
<Icon type="hashtag" className="color-neutral mr-2" />
|
||||
{tag.title
|
||||
.toLowerCase()
|
||||
.split(new RegExp(`(${searchQuery})`, 'gi'))
|
||||
.map((substring, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={
|
||||
substring?.toLowerCase() === searchQuery.toLowerCase()
|
||||
? 'font-bold whitespace-pre-wrap'
|
||||
: 'whitespace-pre-wrap'
|
||||
}
|
||||
>
|
||||
{substring}
|
||||
</span>
|
||||
))}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</DisclosurePanel>
|
||||
)}
|
||||
</Disclosure>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +1,27 @@
|
||||
import { AppState } from "@/ui_models/app_state";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { toDirective } from "./utils";
|
||||
import { Icon } from "./Icon";
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { toDirective } from './utils';
|
||||
import { Icon } from './Icon';
|
||||
import { AutocompleteTagInput } from './AutocompleteTagInput';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
|
||||
type Props = {
|
||||
application: WebApplication;
|
||||
appState: AppState;
|
||||
}
|
||||
};
|
||||
|
||||
const NoteTags = observer(({ appState }: Props) => {
|
||||
const NoteTags = observer(({ application, appState }: Props) => {
|
||||
return (
|
||||
<div className="flex mt-2">
|
||||
{appState.notes.activeNoteTags.map(tag => (
|
||||
<span className="bg-contrast rounded text-sm color-text p-1 flex items-center mr-2">
|
||||
<div className="flex flex-wrap">
|
||||
{appState.notes.activeNoteTags.map((tag) => (
|
||||
<span className="bg-contrast rounded text-xs color-text p-1 flex items-center mt-2 mr-2">
|
||||
<Icon type="hashtag" className="small color-neutral mr-1" />
|
||||
{tag.title}
|
||||
</span>
|
||||
))}
|
||||
<AutocompleteTagInput application={application} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const NoteTagsDirective = toDirective<Props>(NoteTags);
|
||||
export const NoteTagsDirective = toDirective<Props>(NoteTags);
|
||||
|
||||
Reference in New Issue
Block a user