refactor: refactor autocomplete tag input in separate components and move shared logic to state
This commit is contained in:
42
app/assets/javascripts/components/AutocompleteTagHint.tsx
Normal file
42
app/assets/javascripts/components/AutocompleteTagHint.tsx
Normal file
@@ -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 && (
|
||||||
|
<div className="h-1px my-2 bg-border"></div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="sn-dropdown-item"
|
||||||
|
onClick={onTagHintClick}
|
||||||
|
onBlur={closeOnBlur}
|
||||||
|
>
|
||||||
|
<span>Create new tag:</span>
|
||||||
|
<span className="bg-contrast rounded text-xs color-text py-1 pl-1 pr-2 flex items-center ml-2">
|
||||||
|
<Icon
|
||||||
|
type="hashtag"
|
||||||
|
className="sn-icon--small color-neutral mr-1"
|
||||||
|
/>
|
||||||
|
{autocompleteSearchQuery}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -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 { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { Disclosure, DisclosurePanel } from '@reach/disclosure';
|
import { Disclosure, DisclosurePanel } from '@reach/disclosure';
|
||||||
import { useCloseOnBlur } from './utils';
|
import { useCloseOnBlur } from './utils';
|
||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
|
import { AutocompleteTagResult } from './AutocompleteTagResult';
|
||||||
|
import { AutocompleteTagHint } from './AutocompleteTagHint';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication;
|
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
export const AutocompleteTagInput = observer(({ appState }: Props) => {
|
||||||
application,
|
const {
|
||||||
appState,
|
autocompleteSearchQuery,
|
||||||
}) => {
|
autocompleteTagHintVisible,
|
||||||
const { tagElements, tags } = appState.activeNote;
|
autocompleteTagResults,
|
||||||
|
tagElements,
|
||||||
|
tags,
|
||||||
|
} = appState.activeNote;
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
const [dropdownVisible, setDropdownVisible] = useState(false);
|
const [dropdownVisible, setDropdownVisible] = useState(false);
|
||||||
const [dropdownMaxHeight, setDropdownMaxHeight] =
|
const [dropdownMaxHeight, setDropdownMaxHeight] =
|
||||||
useState<number | 'auto'>('auto');
|
useState<number | 'auto'>('auto');
|
||||||
const [hintVisible, setHintVisible] = useState(true);
|
|
||||||
|
|
||||||
const getActiveNoteTagResults = (query: string) => {
|
|
||||||
const { activeNote } = appState.activeNote;
|
|
||||||
return application.searchTags(query, activeNote);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [tagResults, setTagResults] = useState<SNTag[]>(() => {
|
|
||||||
return getActiveNoteTagResults('');
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>();
|
const inputRef = useRef<HTMLInputElement>();
|
||||||
const dropdownRef = useRef<HTMLDivElement>();
|
const dropdownRef = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
const clearResults = () => {
|
const [closeOnBlur] = useCloseOnBlur(dropdownRef, (visible: boolean) => {
|
||||||
setSearchQuery('');
|
setDropdownVisible(visible);
|
||||||
setTagResults(getActiveNoteTagResults(''));
|
appState.activeNote.clearAutocompleteSearch();
|
||||||
};
|
});
|
||||||
|
|
||||||
const [closeOnBlur] = useCloseOnBlur(
|
|
||||||
dropdownRef,
|
|
||||||
(visible: boolean) => {
|
|
||||||
setDropdownVisible(visible);
|
|
||||||
clearResults();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const showDropdown = () => {
|
const showDropdown = () => {
|
||||||
const { clientHeight } = document.documentElement;
|
const { clientHeight } = document.documentElement;
|
||||||
@@ -58,43 +40,29 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
|||||||
|
|
||||||
const onSearchQueryChange = (event: Event) => {
|
const onSearchQueryChange = (event: Event) => {
|
||||||
const query = (event.target as HTMLInputElement).value;
|
const query = (event.target as HTMLInputElement).value;
|
||||||
setTagResults(getActiveNoteTagResults(query));
|
appState.activeNote.setAutocompleteSearchQuery(query);
|
||||||
setSearchQuery(query);
|
appState.activeNote.searchActiveNoteAutocompleteTags();
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFormSubmit = async (event: Event) => {
|
const onFormSubmit = async (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await createAndAddNewTag();
|
await appState.activeNote.createAndAddNewTag();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHintVisible(
|
appState.activeNote.searchActiveNoteAutocompleteTags();
|
||||||
searchQuery !== '' && !tagResults.some((tag) => tag.title === searchQuery)
|
}, [appState.activeNote]);
|
||||||
);
|
|
||||||
}, [tagResults, searchQuery]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onFormSubmit} className={`${tags.length > 0 ? 'mt-2' : ''}`}>
|
<form
|
||||||
|
onSubmit={onFormSubmit}
|
||||||
|
className={`${tags.length > 0 ? 'mt-2' : ''}`}
|
||||||
|
>
|
||||||
<Disclosure open={dropdownVisible} onChange={showDropdown}>
|
<Disclosure open={dropdownVisible} onChange={showDropdown}>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className="w-80 bg-default text-xs color-text no-border h-7 focus:outline-none focus:shadow-none focus:border-bottom"
|
className="w-80 bg-default text-xs color-text no-border h-7 focus:outline-none focus:shadow-none focus:border-bottom"
|
||||||
value={searchQuery}
|
value={autocompleteSearchQuery}
|
||||||
onChange={onSearchQueryChange}
|
onChange={onSearchQueryChange}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Add tag"
|
placeholder="Add tag"
|
||||||
@@ -103,7 +71,7 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
|||||||
onKeyUp={(event) => {
|
onKeyUp={(event) => {
|
||||||
if (
|
if (
|
||||||
event.key === 'Backspace' &&
|
event.key === 'Backspace' &&
|
||||||
searchQuery === '' &&
|
autocompleteSearchQuery === '' &&
|
||||||
tagElements.length > 0
|
tagElements.length > 0
|
||||||
) {
|
) {
|
||||||
tagElements[tagElements.length - 1]?.focus();
|
tagElements[tagElements.length - 1]?.focus();
|
||||||
@@ -117,66 +85,24 @@ export const AutocompleteTagInput: FunctionalComponent<Props> = ({
|
|||||||
style={{ maxHeight: dropdownMaxHeight }}
|
style={{ maxHeight: dropdownMaxHeight }}
|
||||||
>
|
>
|
||||||
<div className="overflow-y-scroll">
|
<div className="overflow-y-scroll">
|
||||||
{tagResults.map((tag) => {
|
{autocompleteTagResults.map((tagResult) => (
|
||||||
return (
|
<AutocompleteTagResult
|
||||||
<button
|
key={tagResult.uuid}
|
||||||
key={tag.uuid}
|
appState={appState}
|
||||||
type="button"
|
tagResult={tagResult}
|
||||||
className="sn-dropdown-item"
|
closeOnBlur={closeOnBlur}
|
||||||
onClick={() => onTagOptionClick(tag)}
|
/>
|
||||||
onBlur={closeOnBlur}
|
))}
|
||||||
>
|
|
||||||
<Icon type="hashtag" className="color-neutral mr-2 min-h-5 min-w-5" />
|
|
||||||
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
|
||||||
{searchQuery === '' ? (
|
|
||||||
tag.title
|
|
||||||
) : (
|
|
||||||
tag.title
|
|
||||||
.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>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
{hintVisible && (
|
{autocompleteTagHintVisible && (
|
||||||
<>
|
<AutocompleteTagHint
|
||||||
{tagResults.length > 0 && (
|
appState={appState}
|
||||||
<div className="h-1px my-2 bg-border"></div>
|
closeOnBlur={closeOnBlur}
|
||||||
)}
|
/>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="sn-dropdown-item"
|
|
||||||
onClick={onTagHintClick}
|
|
||||||
onBlur={closeOnBlur}
|
|
||||||
>
|
|
||||||
<span>Create new tag:</span>
|
|
||||||
<span className="bg-contrast rounded text-xs color-text py-1 pl-1 pr-2 flex items-center ml-2">
|
|
||||||
<Icon
|
|
||||||
type="hashtag"
|
|
||||||
className="sn-icon--small color-neutral mr-1"
|
|
||||||
/>
|
|
||||||
{searchQuery}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</DisclosurePanel>
|
</DisclosurePanel>
|
||||||
)}
|
)}
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|||||||
59
app/assets/javascripts/components/AutocompleteTagResult.tsx
Normal file
59
app/assets/javascripts/components/AutocompleteTagResult.tsx
Normal file
@@ -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 (
|
||||||
|
<button
|
||||||
|
ref={(element) => {
|
||||||
|
if (element) {
|
||||||
|
appState.activeNote.setAutocompleteTagResultElement(
|
||||||
|
tagResult,
|
||||||
|
element
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className="sn-dropdown-item"
|
||||||
|
onClick={() => onTagOptionClick(tagResult)}
|
||||||
|
onBlur={closeOnBlur}
|
||||||
|
>
|
||||||
|
<Icon type="hashtag" className="color-neutral mr-2 min-h-5 min-w-5" />
|
||||||
|
<span className="whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||||
|
{autocompleteSearchQuery === ''
|
||||||
|
? tagResult.title
|
||||||
|
: tagResult.title
|
||||||
|
.split(new RegExp(`(${autocompleteSearchQuery})`, 'gi'))
|
||||||
|
.map((substring, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className={`${
|
||||||
|
substring.toLowerCase() ===
|
||||||
|
autocompleteSearchQuery.toLowerCase()
|
||||||
|
? 'font-bold whitespace-pre-wrap'
|
||||||
|
: 'whitespace-pre-wrap '
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{substring}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
import { FunctionalComponent } from 'preact';
|
|
||||||
import { useRef, useState } from 'preact/hooks';
|
import { useRef, useState } from 'preact/hooks';
|
||||||
import { AppState } from '@/ui_models/app_state';
|
import { AppState } from '@/ui_models/app_state';
|
||||||
import { SNTag } from '@standardnotes/snjs/dist/@types';
|
import { SNTag } from '@standardnotes/snjs/dist/@types';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
tag: SNTag;
|
tag: SNTag;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NoteTag: FunctionalComponent<Props> = ({ appState, tag }) => {
|
export const NoteTag = observer(({ appState, tag }: Props) => {
|
||||||
const {
|
const { tagsContainerMaxWidth } = appState.activeNote;
|
||||||
tagsContainerMaxWidth,
|
|
||||||
} = appState.activeNote;
|
|
||||||
|
|
||||||
const [showDeleteButton, setShowDeleteButton] = useState(false);
|
const [showDeleteButton, setShowDeleteButton] = useState(false);
|
||||||
const deleteTagRef = useRef<HTMLButtonElement>();
|
const deleteTagRef = useRef<HTMLButtonElement>();
|
||||||
@@ -41,14 +39,14 @@ export const NoteTag: FunctionalComponent<Props> = ({ appState, tag }) => {
|
|||||||
let nextTagElement;
|
let nextTagElement;
|
||||||
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case "Backspace":
|
case 'Backspace':
|
||||||
deleteTag();
|
deleteTag();
|
||||||
break;
|
break;
|
||||||
case "ArrowLeft":
|
case 'ArrowLeft':
|
||||||
previousTagElement = appState.activeNote.getPreviousTagElement(tag);
|
previousTagElement = appState.activeNote.getPreviousTagElement(tag);
|
||||||
previousTagElement?.focus();
|
previousTagElement?.focus();
|
||||||
break;
|
break;
|
||||||
case "ArrowRight":
|
case 'ArrowRight':
|
||||||
nextTagElement = appState.activeNote.getNextTagElement(tag);
|
nextTagElement = appState.activeNote.getNextTagElement(tag);
|
||||||
nextTagElement?.focus();
|
nextTagElement?.focus();
|
||||||
break;
|
break;
|
||||||
@@ -92,4 +90,4 @@ export const NoteTag: FunctionalComponent<Props> = ({ appState, tag }) => {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|||||||
@@ -2,16 +2,14 @@ import { AppState } from '@/ui_models/app_state';
|
|||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { toDirective } from './utils';
|
import { toDirective } from './utils';
|
||||||
import { AutocompleteTagInput } from './AutocompleteTagInput';
|
import { AutocompleteTagInput } from './AutocompleteTagInput';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
|
||||||
import { NoteTag } from './NoteTag';
|
import { NoteTag } from './NoteTag';
|
||||||
import { useEffect } from 'preact/hooks';
|
import { useEffect } from 'preact/hooks';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication;
|
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
const NoteTagsContainer = observer(({ appState }: Props) => {
|
||||||
const {
|
const {
|
||||||
tags,
|
tags,
|
||||||
tagsContainerMaxWidth,
|
tagsContainerMaxWidth,
|
||||||
@@ -35,7 +33,7 @@ const NoteTagsContainer = observer(({ application, appState }: Props) => {
|
|||||||
tag={tag}
|
tag={tag}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<AutocompleteTagInput application={application} appState={appState} />
|
<AutocompleteTagInput appState={appState} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import {
|
import { SNNote, ContentType, SNTag } from '@standardnotes/snjs';
|
||||||
SNNote,
|
import { action, computed, makeObservable, observable } from 'mobx';
|
||||||
ContentType,
|
|
||||||
SNTag,
|
|
||||||
} from '@standardnotes/snjs';
|
|
||||||
import {
|
|
||||||
action,
|
|
||||||
makeObservable,
|
|
||||||
observable,
|
|
||||||
} from 'mobx';
|
|
||||||
import { WebApplication } from '../application';
|
import { WebApplication } from '../application';
|
||||||
import { AppState } from './app_state';
|
import { AppState } from './app_state';
|
||||||
|
|
||||||
export class ActiveNoteState {
|
export class ActiveNoteState {
|
||||||
|
autocompleteSearchQuery = '';
|
||||||
|
autocompleteTagResultElements: (HTMLButtonElement | undefined)[] = [];
|
||||||
|
autocompleteTagResults: SNTag[] = [];
|
||||||
tagElements: (HTMLButtonElement | undefined)[] = [];
|
tagElements: (HTMLButtonElement | undefined)[] = [];
|
||||||
tags: SNTag[] = [];
|
tags: SNTag[] = [];
|
||||||
tagsContainerMaxWidth: number | 'auto' = 0;
|
tagsContainerMaxWidth: number | 'auto' = 0;
|
||||||
@@ -22,11 +17,22 @@ export class ActiveNoteState {
|
|||||||
appEventListeners: (() => void)[]
|
appEventListeners: (() => void)[]
|
||||||
) {
|
) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
|
autocompleteSearchQuery: observable,
|
||||||
|
autocompleteTagResultElements: observable,
|
||||||
|
autocompleteTagResults: observable,
|
||||||
tagElements: observable,
|
tagElements: observable,
|
||||||
tags: observable,
|
tags: observable,
|
||||||
tagsContainerMaxWidth: observable,
|
tagsContainerMaxWidth: observable,
|
||||||
|
|
||||||
|
autocompleteTagHintVisible: computed,
|
||||||
|
|
||||||
|
clearAutocompleteSearch: action,
|
||||||
|
setAutocompleteSearchQuery: action,
|
||||||
|
setAutocompleteTagResultElement: action,
|
||||||
|
setAutocompleteTagResultElements: action,
|
||||||
|
setAutocompleteTagResults: action,
|
||||||
setTagElement: action,
|
setTagElement: action,
|
||||||
|
setTagElements: action,
|
||||||
setTags: action,
|
setTags: action,
|
||||||
setTagsContainerMaxWidth: action,
|
setTagsContainerMaxWidth: action,
|
||||||
reloadTags: action,
|
reloadTags: action,
|
||||||
@@ -43,8 +49,41 @@ export class ActiveNoteState {
|
|||||||
return this.appState.notes.activeEditor?.note;
|
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 {
|
setTagElement(tag: SNTag, element: HTMLButtonElement): void {
|
||||||
const tagIndex = this.getTagIndex(tag);
|
const tagIndex = this.getTagIndex(tag, this.tags);
|
||||||
if (tagIndex > -1) {
|
if (tagIndex > -1) {
|
||||||
this.tagElements.splice(tagIndex, 1, element);
|
this.tagElements.splice(tagIndex, 1, element);
|
||||||
}
|
}
|
||||||
@@ -62,19 +101,38 @@ export class ActiveNoteState {
|
|||||||
this.tagsContainerMaxWidth = width;
|
this.tagsContainerMaxWidth = width;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTagIndex(tag: SNTag): number {
|
clearAutocompleteSearch(): void {
|
||||||
return this.tags.findIndex(t => t.uuid === tag.uuid);
|
this.setAutocompleteSearchQuery('');
|
||||||
|
this.searchActiveNoteAutocompleteTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAndAddNewTag(): Promise<void> {
|
||||||
|
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 {
|
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) {
|
if (previousTagIndex > -1 && this.tagElements.length > previousTagIndex) {
|
||||||
return this.tagElements[previousTagIndex];
|
return this.tagElements[previousTagIndex];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getNextTagElement(tag: SNTag): HTMLButtonElement | undefined {
|
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) {
|
if (nextTagIndex > -1 && this.tagElements.length > nextTagIndex) {
|
||||||
return this.tagElements[nextTagIndex];
|
return this.tagElements[nextTagIndex];
|
||||||
}
|
}
|
||||||
@@ -94,9 +152,7 @@ export class ActiveNoteState {
|
|||||||
const EDITOR_ELEMENT_ID = 'editor-column';
|
const EDITOR_ELEMENT_ID = 'editor-column';
|
||||||
const editorWidth = document.getElementById(EDITOR_ELEMENT_ID)?.clientWidth;
|
const editorWidth = document.getElementById(EDITOR_ELEMENT_ID)?.clientWidth;
|
||||||
if (editorWidth) {
|
if (editorWidth) {
|
||||||
this.appState.activeNote.setTagsContainerMaxWidth(
|
this.setTagsContainerMaxWidth(editorWidth);
|
||||||
editorWidth
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@
|
|||||||
ng-if='self.appState.notes.selectedNotesCount > 0'
|
ng-if='self.appState.notes.selectedNotesCount > 0'
|
||||||
)
|
)
|
||||||
note-tags-container(
|
note-tags-container(
|
||||||
application='self.application'
|
|
||||||
app-state='self.appState'
|
app-state='self.appState'
|
||||||
)
|
)
|
||||||
.sn-component(ng-if='self.note')
|
.sn-component(ng-if='self.note')
|
||||||
|
|||||||
Reference in New Issue
Block a user