diff --git a/app/assets/javascripts/components/AutocompleteTagHint.tsx b/app/assets/javascripts/components/AutocompleteTagHint.tsx
new file mode 100644
index 000000000..65630b4f4
--- /dev/null
+++ b/app/assets/javascripts/components/AutocompleteTagHint.tsx
@@ -0,0 +1,42 @@
+import { AppState } from '@/ui_models/app_state';
+import { observer } from 'mobx-react-lite';
+import { Icon } from './Icon';
+
+type Props = {
+ appState: AppState;
+ closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
+};
+
+export const AutocompleteTagHint = observer(
+ ({ appState, closeOnBlur }: Props) => {
+ const { autocompleteSearchQuery, autocompleteTagResults } =
+ appState.activeNote;
+
+ const onTagHintClick = async () => {
+ await appState.activeNote.createAndAddNewTag();
+ };
+
+ return (
+ <>
+ {autocompleteTagResults.length > 0 && (
+
+ )}
+
+ >
+ );
+ }
+);
diff --git a/app/assets/javascripts/components/AutocompleteTagInput.tsx b/app/assets/javascripts/components/AutocompleteTagInput.tsx
index 4fbfab6a0..fa8efc6fc 100644
--- a/app/assets/javascripts/components/AutocompleteTagInput.tsx
+++ b/app/assets/javascripts/components/AutocompleteTagInput.tsx
@@ -1,53 +1,35 @@
-import { WebApplication } from '@/ui_models/application';
-import { SNTag } from '@standardnotes/snjs';
-import { FunctionalComponent } from 'preact';
import { useEffect, useRef, useState } from 'preact/hooks';
-import { Icon } from './Icon';
import { Disclosure, DisclosurePanel } from '@reach/disclosure';
import { useCloseOnBlur } from './utils';
import { AppState } from '@/ui_models/app_state';
+import { AutocompleteTagResult } from './AutocompleteTagResult';
+import { AutocompleteTagHint } from './AutocompleteTagHint';
+import { observer } from 'mobx-react-lite';
type Props = {
- application: WebApplication;
appState: AppState;
};
-export const AutocompleteTagInput: FunctionalComponent = ({
- application,
- appState,
-}) => {
- const { tagElements, tags } = appState.activeNote;
+export const AutocompleteTagInput = observer(({ appState }: Props) => {
+ const {
+ autocompleteSearchQuery,
+ autocompleteTagHintVisible,
+ autocompleteTagResults,
+ tagElements,
+ tags,
+ } = appState.activeNote;
- const [searchQuery, setSearchQuery] = useState('');
const [dropdownVisible, setDropdownVisible] = useState(false);
const [dropdownMaxHeight, setDropdownMaxHeight] =
useState('auto');
- const [hintVisible, setHintVisible] = useState(true);
-
- const getActiveNoteTagResults = (query: string) => {
- const { activeNote } = appState.activeNote;
- return application.searchTags(query, activeNote);
- };
-
- const [tagResults, setTagResults] = useState(() => {
- return getActiveNoteTagResults('');
- });
const inputRef = useRef();
const dropdownRef = useRef();
- const clearResults = () => {
- setSearchQuery('');
- setTagResults(getActiveNoteTagResults(''));
- };
-
- const [closeOnBlur] = useCloseOnBlur(
- dropdownRef,
- (visible: boolean) => {
- setDropdownVisible(visible);
- clearResults();
- }
- );
+ const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => {
+ setDropdownVisible(visible);
+ appState.activeNote.clearAutocompleteSearch();
+ });
const showDropdown = () => {
const { clientHeight } = document.documentElement;
@@ -58,43 +40,29 @@ export const AutocompleteTagInput: FunctionalComponent = ({
const onSearchQueryChange = (event: Event) => {
const query = (event.target as HTMLInputElement).value;
- setTagResults(getActiveNoteTagResults(query));
- setSearchQuery(query);
- };
-
- const onTagOptionClick = async (tag: SNTag) => {
- await appState.activeNote.addTagToActiveNote(tag);
- clearResults();
- };
-
- const createAndAddNewTag = async () => {
- const newTag = await application.findOrCreateTag(searchQuery);
- await appState.activeNote.addTagToActiveNote(newTag);
- clearResults();
- };
-
- const onTagHintClick = async () => {
- await createAndAddNewTag();
+ appState.activeNote.setAutocompleteSearchQuery(query);
+ appState.activeNote.searchActiveNoteAutocompleteTags();
};
const onFormSubmit = async (event: Event) => {
event.preventDefault();
- await createAndAddNewTag();
+ await appState.activeNote.createAndAddNewTag();
};
useEffect(() => {
- setHintVisible(
- searchQuery !== '' && !tagResults.some((tag) => tag.title === searchQuery)
- );
- }, [tagResults, searchQuery]);
+ appState.activeNote.searchActiveNoteAutocompleteTags();
+ }, [appState.activeNote]);
return (
-
);
-};
+});
diff --git a/app/assets/javascripts/components/AutocompleteTagResult.tsx b/app/assets/javascripts/components/AutocompleteTagResult.tsx
new file mode 100644
index 000000000..f61830f16
--- /dev/null
+++ b/app/assets/javascripts/components/AutocompleteTagResult.tsx
@@ -0,0 +1,59 @@
+import { AppState } from '@/ui_models/app_state';
+import { SNTag } from '@standardnotes/snjs';
+import { observer } from 'mobx-react-lite';
+import { Icon } from './Icon';
+
+type Props = {
+ appState: AppState;
+ tagResult: SNTag;
+ closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void;
+};
+
+export const AutocompleteTagResult = observer(
+ ({ appState, tagResult, closeOnBlur }: Props) => {
+ const { autocompleteSearchQuery } = appState.activeNote;
+
+ const onTagOptionClick = async (tag: SNTag) => {
+ await appState.activeNote.addTagToActiveNote(tag);
+ appState.activeNote.clearAutocompleteSearch();
+ };
+
+ return (
+
+ );
+ }
+);
diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx
index 36c9bba8a..a0bf2f73c 100644
--- a/app/assets/javascripts/components/NoteTag.tsx
+++ b/app/assets/javascripts/components/NoteTag.tsx
@@ -1,18 +1,16 @@
import { Icon } from './Icon';
-import { FunctionalComponent } from 'preact';
import { useRef, useState } from 'preact/hooks';
import { AppState } from '@/ui_models/app_state';
import { SNTag } from '@standardnotes/snjs/dist/@types';
+import { observer } from 'mobx-react-lite';
type Props = {
appState: AppState;
tag: SNTag;
};
-export const NoteTag: FunctionalComponent = ({ appState, tag }) => {
- const {
- tagsContainerMaxWidth,
- } = appState.activeNote;
+export const NoteTag = observer(({ appState, tag }: Props) => {
+ const { tagsContainerMaxWidth } = appState.activeNote;
const [showDeleteButton, setShowDeleteButton] = useState(false);
const deleteTagRef = useRef();
@@ -41,14 +39,14 @@ export const NoteTag: FunctionalComponent = ({ appState, tag }) => {
let nextTagElement;
switch (event.key) {
- case "Backspace":
+ case 'Backspace':
deleteTag();
break;
- case "ArrowLeft":
+ case 'ArrowLeft':
previousTagElement = appState.activeNote.getPreviousTagElement(tag);
previousTagElement?.focus();
break;
- case "ArrowRight":
+ case 'ArrowRight':
nextTagElement = appState.activeNote.getNextTagElement(tag);
nextTagElement?.focus();
break;
@@ -92,4 +90,4 @@ export const NoteTag: FunctionalComponent = ({ appState, tag }) => {
)}
);
-};
+});
diff --git a/app/assets/javascripts/components/NoteTagsContainer.tsx b/app/assets/javascripts/components/NoteTagsContainer.tsx
index ff0d2e087..dbf73fceb 100644
--- a/app/assets/javascripts/components/NoteTagsContainer.tsx
+++ b/app/assets/javascripts/components/NoteTagsContainer.tsx
@@ -2,16 +2,14 @@ import { AppState } from '@/ui_models/app_state';
import { observer } from 'mobx-react-lite';
import { toDirective } from './utils';
import { AutocompleteTagInput } from './AutocompleteTagInput';
-import { WebApplication } from '@/ui_models/application';
import { NoteTag } from './NoteTag';
import { useEffect } from 'preact/hooks';
type Props = {
- application: WebApplication;
appState: AppState;
};
-const NoteTagsContainer = observer(({ application, appState }: Props) => {
+const NoteTagsContainer = observer(({ appState }: Props) => {
const {
tags,
tagsContainerMaxWidth,
@@ -35,7 +33,7 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
tag={tag}
/>
))}
-
+
);
});
diff --git a/app/assets/javascripts/ui_models/app_state/active_note_state.ts b/app/assets/javascripts/ui_models/app_state/active_note_state.ts
index 775120d21..c7a0dc124 100644
--- a/app/assets/javascripts/ui_models/app_state/active_note_state.ts
+++ b/app/assets/javascripts/ui_models/app_state/active_note_state.ts
@@ -1,17 +1,12 @@
-import {
- SNNote,
- ContentType,
- SNTag,
-} from '@standardnotes/snjs';
-import {
- action,
- makeObservable,
- observable,
-} from 'mobx';
+import { SNNote, ContentType, SNTag } from '@standardnotes/snjs';
+import { action, computed, makeObservable, observable } from 'mobx';
import { WebApplication } from '../application';
import { AppState } from './app_state';
export class ActiveNoteState {
+ autocompleteSearchQuery = '';
+ autocompleteTagResultElements: (HTMLButtonElement | undefined)[] = [];
+ autocompleteTagResults: SNTag[] = [];
tagElements: (HTMLButtonElement | undefined)[] = [];
tags: SNTag[] = [];
tagsContainerMaxWidth: number | 'auto' = 0;
@@ -22,11 +17,22 @@ export class ActiveNoteState {
appEventListeners: (() => void)[]
) {
makeObservable(this, {
+ autocompleteSearchQuery: observable,
+ autocompleteTagResultElements: observable,
+ autocompleteTagResults: observable,
tagElements: observable,
tags: observable,
tagsContainerMaxWidth: observable,
+ autocompleteTagHintVisible: computed,
+
+ clearAutocompleteSearch: action,
+ setAutocompleteSearchQuery: action,
+ setAutocompleteTagResultElement: action,
+ setAutocompleteTagResultElements: action,
+ setAutocompleteTagResults: action,
setTagElement: action,
+ setTagElements: action,
setTags: action,
setTagsContainerMaxWidth: action,
reloadTags: action,
@@ -43,8 +49,41 @@ export class ActiveNoteState {
return this.appState.notes.activeEditor?.note;
}
+ get autocompleteTagHintVisible(): boolean {
+ return (
+ this.autocompleteSearchQuery !== '' &&
+ !this.autocompleteTagResults.some(
+ (tagResult) => tagResult.title === this.autocompleteSearchQuery
+ )
+ );
+ }
+
+ setAutocompleteSearchQuery(query: string): void {
+ this.autocompleteSearchQuery = query;
+ }
+
+ setAutocompleteTagResultElement(
+ tagResult: SNTag,
+ element: HTMLButtonElement
+ ): void {
+ const tagIndex = this.getTagIndex(tagResult, this.autocompleteTagResults);
+ if (tagIndex > -1) {
+ this.autocompleteTagResultElements.splice(tagIndex, 1, element);
+ }
+ }
+
+ setAutocompleteTagResultElements(
+ elements: (HTMLButtonElement | undefined)[]
+ ): void {
+ this.autocompleteTagResultElements = elements;
+ }
+
+ setAutocompleteTagResults(results: SNTag[]): void {
+ this.autocompleteTagResults = results;
+ }
+
setTagElement(tag: SNTag, element: HTMLButtonElement): void {
- const tagIndex = this.getTagIndex(tag);
+ const tagIndex = this.getTagIndex(tag, this.tags);
if (tagIndex > -1) {
this.tagElements.splice(tagIndex, 1, element);
}
@@ -62,19 +101,38 @@ export class ActiveNoteState {
this.tagsContainerMaxWidth = width;
}
- getTagIndex(tag: SNTag): number {
- return this.tags.findIndex(t => t.uuid === tag.uuid);
+ clearAutocompleteSearch(): void {
+ this.setAutocompleteSearchQuery('');
+ this.searchActiveNoteAutocompleteTags();
+ }
+
+ async createAndAddNewTag(): Promise {
+ const newTag = await this.application.findOrCreateTag(this.autocompleteSearchQuery);
+ await this.addTagToActiveNote(newTag);
+ this.clearAutocompleteSearch();
+ }
+
+ searchActiveNoteAutocompleteTags(): void {
+ const newResults = this.application.searchTags(
+ this.autocompleteSearchQuery,
+ this.activeNote
+ );
+ this.setAutocompleteTagResults(newResults);
+ }
+
+ getTagIndex(tag: SNTag, tagsArr: SNTag[]): number {
+ return tagsArr.findIndex((t) => t.uuid === tag.uuid);
}
getPreviousTagElement(tag: SNTag): HTMLButtonElement | undefined {
- const previousTagIndex = this.getTagIndex(tag) - 1;
+ const previousTagIndex = this.getTagIndex(tag, this.tags) - 1;
if (previousTagIndex > -1 && this.tagElements.length > previousTagIndex) {
return this.tagElements[previousTagIndex];
}
}
getNextTagElement(tag: SNTag): HTMLButtonElement | undefined {
- const nextTagIndex = this.getTagIndex(tag) + 1;
+ const nextTagIndex = this.getTagIndex(tag, this.tags) + 1;
if (nextTagIndex > -1 && this.tagElements.length > nextTagIndex) {
return this.tagElements[nextTagIndex];
}
@@ -94,9 +152,7 @@ export class ActiveNoteState {
const EDITOR_ELEMENT_ID = 'editor-column';
const editorWidth = document.getElementById(EDITOR_ELEMENT_ID)?.clientWidth;
if (editorWidth) {
- this.appState.activeNote.setTagsContainerMaxWidth(
- editorWidth
- );
+ this.setTagsContainerMaxWidth(editorWidth);
}
}
diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug
index 9cb2f8e4c..dcaa5b45d 100644
--- a/app/assets/javascripts/views/editor/editor-view.pug
+++ b/app/assets/javascripts/views/editor/editor-view.pug
@@ -53,7 +53,6 @@
ng-if='self.appState.notes.selectedNotesCount > 0'
)
note-tags-container(
- application='self.application'
app-state='self.appState'
)
.sn-component(ng-if='self.note')