diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx new file mode 100644 index 000000000..9eb089953 --- /dev/null +++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx @@ -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 = ({ + application, +}) => { + const [searchQuery, setSearchQuery] = useState(''); + const [tagResults, setTagResults] = useState(() => { + return application.searchTags(''); + }); + const [dropdownVisible, setDropdownVisible] = useState(false); + + const dropdownRef = useRef(); + 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 ( +
event.preventDefault()} className="mt-2"> + setDropdownVisible(true)} + > + { + if (tagResults.length > 0) { + setDropdownVisible(true); + } + }} + /> + {dropdownVisible && ( + + {tagResults.map((tag) => { + return ( + + ); + })} + + )} + +
+ ); +}; diff --git a/app/assets/javascripts/components/NoteTags.tsx b/app/assets/javascripts/components/NoteTags.tsx index 8b78e0ff6..015185ef5 100644 --- a/app/assets/javascripts/components/NoteTags.tsx +++ b/app/assets/javascripts/components/NoteTags.tsx @@ -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 ( -
- {appState.notes.activeNoteTags.map(tag => ( - +
+ {appState.notes.activeNoteTags.map((tag) => ( + {tag.title} ))} +
); }); -export const NoteTagsDirective = toDirective(NoteTags); \ No newline at end of file +export const NoteTagsDirective = toDirective(NoteTags); diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug index dabb895c3..3a86f047f 100644 --- a/app/assets/javascripts/views/editor/editor-view.pug +++ b/app/assets/javascripts/views/editor/editor-view.pug @@ -50,6 +50,7 @@ ) div.flex note-tags( + application='self.application' app-state='self.appState' ) input.tags-input( diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 8f474af45..c6e092940 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -40,6 +40,10 @@ border-color: var(--sn-stylekit-background-color); } +.focus\:border-bottom:focus { + border-bottom: 2px solid var(--sn-stylekit-info-color); +} + .grid { display: grid; } @@ -155,6 +159,10 @@ height: 1.25rem; } +.h-7 { + height: 1.75rem; +} + .h-8 { height: 2rem; } @@ -187,6 +195,14 @@ padding: 0.5rem; } +.flex-wrap { + flex-wrap: wrap; +} + +.whitespace-pre-wrap { + white-space: pre-wrap; +} + /** * A button that is just an icon. Separated from .sn-button because there * is almost no style overlap.