feat: note tags button for mobile (#1641)

This commit is contained in:
Aman Harwara
2022-09-26 02:58:55 +05:30
committed by GitHub
parent 4985787c5d
commit fe2ce9f1e8
8 changed files with 273 additions and 93 deletions

View File

@@ -8,21 +8,23 @@ import {
useRef, useRef,
useState, useState,
} from 'react' } from 'react'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { SNTag } from '@standardnotes/snjs' import { SNTag } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider' import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata' import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
import { NoteTagsController } from '@/Controllers/NoteTagsController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
type Props = { type Props = {
viewControllerManager: ViewControllerManager noteTagsController: NoteTagsController
navigationController: NavigationController
tag: SNTag tag: SNTag
} }
const NoteTag = ({ viewControllerManager, tag }: Props) => { const NoteTag = ({ noteTagsController, navigationController, tag }: Props) => {
const { toggleAppPane } = useResponsiveAppPane() const { toggleAppPane } = useResponsiveAppPane()
const noteTags = viewControllerManager.noteTagsController const noteTags = noteTagsController
const { autocompleteInputFocused, focusedTagUuid, tags } = noteTags const { autocompleteInputFocused, focusedTagUuid, tags } = noteTags
@@ -37,9 +39,9 @@ const NoteTag = ({ viewControllerManager, tag }: Props) => {
const longTitle = noteTags.getLongTitle(tag) const longTitle = noteTags.getLongTitle(tag)
const deleteTag = useCallback(() => { const deleteTag = useCallback(() => {
viewControllerManager.noteTagsController.focusPreviousTag(tag) noteTagsController.focusPreviousTag(tag)
viewControllerManager.noteTagsController.removeTagFromActiveNote(tag).catch(console.error) noteTagsController.removeTagFromActiveNote(tag).catch(console.error)
}, [viewControllerManager, tag]) }, [noteTagsController, tag])
const onDeleteTagClick: MouseEventHandler = useCallback( const onDeleteTagClick: MouseEventHandler = useCallback(
(event) => { (event) => {
@@ -53,30 +55,30 @@ const NoteTag = ({ viewControllerManager, tag }: Props) => {
async (event) => { async (event) => {
if (tagClicked && event.target !== deleteTagRef.current) { if (tagClicked && event.target !== deleteTagRef.current) {
setTagClicked(false) setTagClicked(false)
await viewControllerManager.navigationController.setSelectedTag(tag) await navigationController.setSelectedTag(tag)
toggleAppPane(AppPaneId.Items) toggleAppPane(AppPaneId.Items)
} else { } else {
setTagClicked(true) setTagClicked(true)
tagRef.current?.focus() tagRef.current?.focus()
} }
}, },
[viewControllerManager, tagClicked, tag, toggleAppPane], [tagClicked, navigationController, tag, toggleAppPane],
) )
const onFocus = useCallback(() => { const onFocus = useCallback(() => {
viewControllerManager.noteTagsController.setFocusedTagUuid(tag.uuid) noteTagsController.setFocusedTagUuid(tag.uuid)
setShowDeleteButton(true) setShowDeleteButton(true)
}, [viewControllerManager, tag]) }, [noteTagsController, tag])
const onBlur: FocusEventHandler = useCallback( const onBlur: FocusEventHandler = useCallback(
(event) => { (event) => {
const relatedTarget = event.relatedTarget as Node const relatedTarget = event.relatedTarget as Node
if (relatedTarget !== deleteTagRef.current) { if (relatedTarget !== deleteTagRef.current) {
viewControllerManager.noteTagsController.setFocusedTagUuid(undefined) noteTagsController.setFocusedTagUuid(undefined)
setShowDeleteButton(false) setShowDeleteButton(false)
} }
}, },
[viewControllerManager], [noteTagsController],
) )
const getTabIndex = useCallback(() => { const getTabIndex = useCallback(() => {
@@ -91,33 +93,33 @@ const NoteTag = ({ viewControllerManager, tag }: Props) => {
const onKeyDown: KeyboardEventHandler = useCallback( const onKeyDown: KeyboardEventHandler = useCallback(
(event) => { (event) => {
const tagIndex = viewControllerManager.noteTagsController.getTagIndex(tag, tags) const tagIndex = noteTagsController.getTagIndex(tag, tags)
switch (event.key) { switch (event.key) {
case 'Backspace': case 'Backspace':
deleteTag() deleteTag()
break break
case 'ArrowLeft': case 'ArrowLeft':
viewControllerManager.noteTagsController.focusPreviousTag(tag) noteTagsController.focusPreviousTag(tag)
break break
case 'ArrowRight': case 'ArrowRight':
if (tagIndex === tags.length - 1) { if (tagIndex === tags.length - 1) {
viewControllerManager.noteTagsController.setAutocompleteInputFocused(true) noteTagsController.setAutocompleteInputFocused(true)
} else { } else {
viewControllerManager.noteTagsController.focusNextTag(tag) noteTagsController.focusNextTag(tag)
} }
break break
default: default:
return return
} }
}, },
[viewControllerManager, deleteTag, tag, tags], [noteTagsController, deleteTag, tag, tags],
) )
useEffect(() => { useEffect(() => {
if (focusedTagUuid === tag.uuid) { if (focusedTagUuid === tag.uuid) {
tagRef.current?.focus() tagRef.current?.focus()
} }
}, [viewControllerManager, focusedTagUuid, tag]) }, [noteTagsController, focusedTagUuid, tag])
return ( return (
<button <button

View File

@@ -1,31 +1,33 @@
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import AutocompleteTagInput from '@/Components/TagAutocomplete/AutocompleteTagInput' import AutocompleteTagInput from '@/Components/TagAutocomplete/AutocompleteTagInput'
import NoteTag from './NoteTag' import NoteTag from './NoteTag'
import { useEffect } from 'react' import { useEffect } from 'react'
import { NoteTagsController } from '@/Controllers/NoteTagsController'
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
type Props = { type Props = {
viewControllerManager: ViewControllerManager noteTagsController: NoteTagsController
navigationController: NavigationController
} }
const NoteTagsContainer = ({ viewControllerManager }: Props) => { const NoteTagsContainer = ({ noteTagsController, navigationController }: Props) => {
const { tags, tagsContainerMaxWidth } = viewControllerManager.noteTagsController const { tags } = noteTagsController
useEffect(() => { useEffect(() => {
viewControllerManager.noteTagsController.reloadTagsContainerMaxWidth() noteTagsController.reloadTagsContainerMaxWidth()
}, [viewControllerManager]) }, [noteTagsController])
return ( return (
<div <div className="hidden min-w-80 max-w-full flex-wrap bg-transparent md:-mr-2 md:flex">
className="flex min-w-80 flex-wrap bg-transparent md:-mr-2"
style={{
maxWidth: tagsContainerMaxWidth,
}}
>
{tags.map((tag) => ( {tags.map((tag) => (
<NoteTag key={tag.uuid} viewControllerManager={viewControllerManager} tag={tag} /> <NoteTag
key={tag.uuid}
noteTagsController={noteTagsController}
navigationController={navigationController}
tag={tag}
/>
))} ))}
<AutocompleteTagInput viewControllerManager={viewControllerManager} /> <AutocompleteTagInput noteTagsController={noteTagsController} />
</div> </div>
) )
} }

View File

@@ -0,0 +1,170 @@
import { NoteTagsController } from '@/Controllers/NoteTagsController'
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { splitQueryInString } from '@/Utils'
import { SNTag } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { ChangeEventHandler, FormEventHandler, useCallback, useEffect, useRef, useState } from 'react'
import Icon from '../Icon/Icon'
import Popover from '../Popover/Popover'
const ListItem = ({
tag,
isSearching,
noteTagsController,
autocompleteSearchQuery,
}: {
tag: SNTag
isSearching: boolean
noteTagsController: NoteTagsController
autocompleteSearchQuery: string
}) => {
const handleSearchResultClick = useCallback(async () => {
await noteTagsController.addTagToActiveNote(tag)
noteTagsController.clearAutocompleteSearch()
noteTagsController.setAutocompleteInputFocused(true)
}, [noteTagsController, tag])
const handleNoteTagRemove = useCallback(() => {
noteTagsController.removeTagFromActiveNote(tag).catch(console.error)
}, [noteTagsController, tag])
const longTitle = noteTagsController.getLongTitle(tag)
return isSearching ? (
<button
onClick={handleSearchResultClick}
className="max-w-80 flex w-full items-center border-0 bg-transparent px-3 py-2 text-left text-menu-item text-text hover:bg-info-backdrop focus:bg-info-backdrop"
>
{splitQueryInString(longTitle, autocompleteSearchQuery).map((substring, index) => (
<span
key={index}
className={
substring.toLowerCase() === autocompleteSearchQuery.toLowerCase()
? 'whitespace-pre-wrap font-bold'
: 'whitespace-pre-wrap'
}
>
{substring}
</span>
))}
</button>
) : (
<div className="max-w-80 flex w-full items-center justify-between border-0 bg-transparent px-3 py-2 text-left text-menu-item text-text">
<span className="overflow-hidden overflow-ellipsis whitespace-nowrap">{longTitle}</span>
<button
onClick={handleNoteTagRemove}
className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-full text-danger hover:bg-info-backdrop focus:bg-info-backdrop"
>
<Icon type="trash" size="small" />
</button>
</div>
)
}
const NoteTagsPanel = ({
noteTagsController,
onClickPreprocessing,
}: {
noteTagsController: NoteTagsController
onClickPreprocessing?: () => Promise<void>
}) => {
const isDesktopScreen = useMediaQuery(MediaQueryBreakpoints.md)
const [isOpen, setIsOpen] = useState(false)
const buttonRef = useRef<HTMLButtonElement>(null)
const { tags, autocompleteTagResults, autocompleteSearchQuery, autocompleteTagHintVisible } = noteTagsController
const isSearching = autocompleteSearchQuery.length > 0
const visibleTagsList = isSearching ? autocompleteTagResults : tags
const toggleMenu = useCallback(async () => {
const willMenuOpen = !isOpen
if (willMenuOpen && onClickPreprocessing) {
await onClickPreprocessing()
}
setIsOpen(willMenuOpen)
}, [onClickPreprocessing, isOpen])
const onSearchQueryChange: ChangeEventHandler<HTMLInputElement> = (event) => {
const query = event.target.value
if (query === '') {
noteTagsController.clearAutocompleteSearch()
} else {
noteTagsController.setAutocompleteSearchQuery(query)
noteTagsController.searchActiveNoteAutocompleteTags()
}
}
const onFormSubmit: FormEventHandler = async (event) => {
event.preventDefault()
if (autocompleteSearchQuery !== '') {
await noteTagsController.createAndAddNewTag()
}
}
useEffect(() => {
if (isDesktopScreen) {
setIsOpen(false)
}
}, [isDesktopScreen])
return (
<>
<button
className="bg-text-padding flex h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-border text-neutral hover:bg-contrast focus:bg-contrast md:hidden"
title="Note options menu"
aria-label="Note options menu"
onClick={toggleMenu}
ref={buttonRef}
>
<Icon type="hashtag" />
</button>
<Popover togglePopover={toggleMenu} anchorElement={buttonRef.current} open={isOpen} className="pb-2">
<form onSubmit={onFormSubmit} className="sticky top-0 border-b border-border bg-default px-2.5 py-2.5">
<input
type="text"
className="w-full rounded border border-solid border-border bg-default py-1.5 px-3 text-sm text-text"
placeholder="Create or search tag..."
value={autocompleteSearchQuery}
onChange={onSearchQueryChange}
ref={(node) => {
if (isOpen && node) {
node.focus()
}
}}
/>
</form>
<div className="pt-2.5">
{visibleTagsList.map((tag) => (
<ListItem
key={tag.uuid}
tag={tag}
isSearching={isSearching}
noteTagsController={noteTagsController}
autocompleteSearchQuery={autocompleteSearchQuery}
/>
))}
{autocompleteTagHintVisible && (
<button
onClick={async () => {
await noteTagsController.createAndAddNewTag()
}}
className="max-w-80 flex w-full items-center border-0 bg-transparent px-3 py-2 text-left text-menu-item text-text hover:bg-info-backdrop focus:bg-info-backdrop"
>
<span>Create new tag:</span>
<span className="ml-2 flex items-center rounded bg-contrast py-1 pl-1 pr-2 text-xs text-text">
<Icon type="hashtag" className="mr-1 text-neutral" size="small" />
<span className="max-w-40 overflow-hidden overflow-ellipsis whitespace-nowrap">
{autocompleteSearchQuery}
</span>
</span>
</button>
)}
</div>
</Popover>
</>
)
}
export default observer(NoteTagsPanel)

View File

@@ -39,6 +39,7 @@ import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
import { classNames } from '@/Utils/ConcatenateClassNames' import { classNames } from '@/Utils/ConcatenateClassNames'
import AutoresizingNoteViewTextarea from './AutoresizingTextarea' import AutoresizingNoteViewTextarea from './AutoresizingTextarea'
import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton' import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton'
import NoteTagsPanel from '../NoteTags/NoteTagsPanel'
const MINIMUM_STATUS_DURATION = 400 const MINIMUM_STATUS_DURATION = 400
const TEXTAREA_DEBOUNCE = 100 const TEXTAREA_DEBOUNCE = 100
@@ -944,6 +945,10 @@ class NoteView extends PureComponent<NoteViewProps, State> {
</div> </div>
)} )}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<NoteTagsPanel
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
noteTagsController={this.viewControllerManager.noteTagsController}
/>
<AttachedFilesButton <AttachedFilesButton
application={this.application} application={this.application}
onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction} onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction}
@@ -974,7 +979,10 @@ class NoteView extends PureComponent<NoteViewProps, State> {
</div> </div>
</div> </div>
</div> </div>
<NoteTagsContainer viewControllerManager={this.viewControllerManager} /> <NoteTagsContainer
noteTagsController={this.viewControllerManager.noteTagsController}
navigationController={this.viewControllerManager.navigationController}
/>
</div> </div>
)} )}

View File

@@ -71,17 +71,15 @@ const PositionedPopoverContent = ({
}} }}
data-popover={id} data-popover={id}
> >
<div className={className}> <div className="md:hidden">
<div className="md:hidden"> <div className="flex items-center justify-end px-3 pt-2">
<div className="flex items-center justify-end px-3"> <button className="rounded-full border border-border p-1" onClick={togglePopover}>
<button className="rounded-full border border-border p-1" onClick={togglePopover}> <Icon type="close" className="h-4 w-4" />
<Icon type="close" className="h-4 w-4" /> </button>
</button>
</div>
<HorizontalSeparator classes="my-2" />
</div> </div>
{children} <HorizontalSeparator classes="my-2" />
</div> </div>
<div className={className}>{children}</div>
</div> </div>
</Portal> </Portal>
) )

View File

@@ -1,36 +1,36 @@
import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { NoteTagsController } from '@/Controllers/NoteTagsController'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useRef, useEffect, useCallback, FocusEventHandler, KeyboardEventHandler } from 'react' import { useRef, useEffect, useCallback, FocusEventHandler, KeyboardEventHandler } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import HorizontalSeparator from '../Shared/HorizontalSeparator' import HorizontalSeparator from '../Shared/HorizontalSeparator'
type Props = { type Props = {
viewControllerManager: ViewControllerManager noteTagsController: NoteTagsController
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
} }
const AutocompleteTagHint = ({ viewControllerManager, closeOnBlur }: Props) => { const AutocompleteTagHint = ({ noteTagsController, closeOnBlur }: Props) => {
const { autocompleteTagHintFocused } = viewControllerManager.noteTagsController const { autocompleteTagHintFocused } = noteTagsController
const hintRef = useRef<HTMLButtonElement>(null) const hintRef = useRef<HTMLButtonElement>(null)
const { autocompleteSearchQuery, autocompleteTagResults } = viewControllerManager.noteTagsController const { autocompleteSearchQuery, autocompleteTagResults } = noteTagsController
const onTagHintClick = useCallback(async () => { const onTagHintClick = useCallback(async () => {
await viewControllerManager.noteTagsController.createAndAddNewTag() await noteTagsController.createAndAddNewTag()
viewControllerManager.noteTagsController.setAutocompleteInputFocused(true) noteTagsController.setAutocompleteInputFocused(true)
}, [viewControllerManager]) }, [noteTagsController])
const onFocus = useCallback(() => { const onFocus = useCallback(() => {
viewControllerManager.noteTagsController.setAutocompleteTagHintFocused(true) noteTagsController.setAutocompleteTagHintFocused(true)
}, [viewControllerManager]) }, [noteTagsController])
const onBlur: FocusEventHandler = useCallback( const onBlur: FocusEventHandler = useCallback(
(event) => { (event) => {
closeOnBlur(event) closeOnBlur(event)
viewControllerManager.noteTagsController.setAutocompleteTagHintFocused(false) noteTagsController.setAutocompleteTagHintFocused(false)
}, },
[viewControllerManager, closeOnBlur], [noteTagsController, closeOnBlur],
) )
const onKeyDown: KeyboardEventHandler = useCallback( const onKeyDown: KeyboardEventHandler = useCallback(
@@ -38,20 +38,20 @@ const AutocompleteTagHint = ({ viewControllerManager, closeOnBlur }: Props) => {
if (event.key === 'ArrowUp') { if (event.key === 'ArrowUp') {
if (autocompleteTagResults.length > 0) { if (autocompleteTagResults.length > 0) {
const lastTagResult = autocompleteTagResults[autocompleteTagResults.length - 1] const lastTagResult = autocompleteTagResults[autocompleteTagResults.length - 1]
viewControllerManager.noteTagsController.setFocusedTagResultUuid(lastTagResult.uuid) noteTagsController.setFocusedTagResultUuid(lastTagResult.uuid)
} else { } else {
viewControllerManager.noteTagsController.setAutocompleteInputFocused(true) noteTagsController.setAutocompleteInputFocused(true)
} }
} }
}, },
[viewControllerManager, autocompleteTagResults], [noteTagsController, autocompleteTagResults],
) )
useEffect(() => { useEffect(() => {
if (autocompleteTagHintFocused) { if (autocompleteTagHintFocused) {
hintRef.current?.focus() hintRef.current?.focus()
} }
}, [viewControllerManager, autocompleteTagHintFocused]) }, [noteTagsController, autocompleteTagHintFocused])
return ( return (
<> <>

View File

@@ -9,19 +9,19 @@ import {
} from 'react' } from 'react'
import { Disclosure, DisclosurePanel } from '@reach/disclosure' import { Disclosure, DisclosurePanel } from '@reach/disclosure'
import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur'
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import AutocompleteTagResult from './AutocompleteTagResult' import AutocompleteTagResult from './AutocompleteTagResult'
import AutocompleteTagHint from './AutocompleteTagHint' import AutocompleteTagHint from './AutocompleteTagHint'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { SNTag } from '@standardnotes/snjs' import { SNTag } from '@standardnotes/snjs'
import { classNames } from '@/Utils/ConcatenateClassNames' import { classNames } from '@/Utils/ConcatenateClassNames'
import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
import { NoteTagsController } from '@/Controllers/NoteTagsController'
type Props = { type Props = {
viewControllerManager: ViewControllerManager noteTagsController: NoteTagsController
} }
const AutocompleteTagInput = ({ viewControllerManager }: Props) => { const AutocompleteTagInput = ({ noteTagsController }: Props) => {
const { const {
autocompleteInputFocused, autocompleteInputFocused,
autocompleteSearchQuery, autocompleteSearchQuery,
@@ -29,7 +29,7 @@ const AutocompleteTagInput = ({ viewControllerManager }: Props) => {
autocompleteTagResults, autocompleteTagResults,
tags, tags,
tagsContainerMaxWidth, tagsContainerMaxWidth,
} = viewControllerManager.noteTagsController } = noteTagsController
const [dropdownVisible, setDropdownVisible] = useState(false) const [dropdownVisible, setDropdownVisible] = useState(false)
const [dropdownMaxHeight, setDropdownMaxHeight] = useState<number | 'auto'>('auto') const [dropdownMaxHeight, setDropdownMaxHeight] = useState<number | 'auto'>('auto')
@@ -39,7 +39,7 @@ const AutocompleteTagInput = ({ viewControllerManager }: Props) => {
const [closeOnBlur] = useCloseOnBlur(containerRef, (visible: boolean) => { const [closeOnBlur] = useCloseOnBlur(containerRef, (visible: boolean) => {
setDropdownVisible(visible) setDropdownVisible(visible)
viewControllerManager.noteTagsController.clearAutocompleteSearch() noteTagsController.clearAutocompleteSearch()
}) })
const showDropdown = () => { const showDropdown = () => {
@@ -55,17 +55,17 @@ const AutocompleteTagInput = ({ viewControllerManager }: Props) => {
const query = event.target.value const query = event.target.value
if (query === '') { if (query === '') {
viewControllerManager.noteTagsController.clearAutocompleteSearch() noteTagsController.clearAutocompleteSearch()
} else { } else {
viewControllerManager.noteTagsController.setAutocompleteSearchQuery(query) noteTagsController.setAutocompleteSearchQuery(query)
viewControllerManager.noteTagsController.searchActiveNoteAutocompleteTags() noteTagsController.searchActiveNoteAutocompleteTags()
} }
} }
const onFormSubmit: FormEventHandler = async (event) => { const onFormSubmit: FormEventHandler = async (event) => {
event.preventDefault() event.preventDefault()
if (autocompleteSearchQuery !== '') { if (autocompleteSearchQuery !== '') {
await viewControllerManager.noteTagsController.createAndAddNewTag() await noteTagsController.createAndAddNewTag()
} }
} }
@@ -74,15 +74,15 @@ const AutocompleteTagInput = ({ viewControllerManager }: Props) => {
case 'Backspace': case 'Backspace':
case 'ArrowLeft': case 'ArrowLeft':
if (autocompleteSearchQuery === '' && tags.length > 0) { if (autocompleteSearchQuery === '' && tags.length > 0) {
viewControllerManager.noteTagsController.setFocusedTagUuid(tags[tags.length - 1].uuid) noteTagsController.setFocusedTagUuid(tags[tags.length - 1].uuid)
} }
break break
case 'ArrowDown': case 'ArrowDown':
event.preventDefault() event.preventDefault()
if (autocompleteTagResults.length > 0) { if (autocompleteTagResults.length > 0) {
viewControllerManager.noteTagsController.setFocusedTagResultUuid(autocompleteTagResults[0].uuid) noteTagsController.setFocusedTagResultUuid(autocompleteTagResults[0].uuid)
} else if (autocompleteTagHintVisible) { } else if (autocompleteTagHintVisible) {
viewControllerManager.noteTagsController.setAutocompleteTagHintFocused(true) noteTagsController.setAutocompleteTagHintFocused(true)
} }
break break
default: default:
@@ -92,19 +92,19 @@ const AutocompleteTagInput = ({ viewControllerManager }: Props) => {
const onFocus = () => { const onFocus = () => {
showDropdown() showDropdown()
viewControllerManager.noteTagsController.setAutocompleteInputFocused(true) noteTagsController.setAutocompleteInputFocused(true)
} }
const onBlur: FocusEventHandler = (event) => { const onBlur: FocusEventHandler = (event) => {
closeOnBlur(event) closeOnBlur(event)
viewControllerManager.noteTagsController.setAutocompleteInputFocused(false) noteTagsController.setAutocompleteInputFocused(false)
} }
useEffect(() => { useEffect(() => {
if (autocompleteInputFocused) { if (autocompleteInputFocused) {
inputRef.current?.focus() inputRef.current?.focus()
} }
}, [viewControllerManager, autocompleteInputFocused]) }, [autocompleteInputFocused])
return ( return (
<div ref={containerRef}> <div ref={containerRef}>
@@ -140,14 +140,14 @@ const AutocompleteTagInput = ({ viewControllerManager }: Props) => {
{autocompleteTagResults.map((tagResult: SNTag) => ( {autocompleteTagResults.map((tagResult: SNTag) => (
<AutocompleteTagResult <AutocompleteTagResult
key={tagResult.uuid} key={tagResult.uuid}
viewControllerManager={viewControllerManager} noteTagsController={noteTagsController}
tagResult={tagResult} tagResult={tagResult}
closeOnBlur={closeOnBlur} closeOnBlur={closeOnBlur}
/> />
))} ))}
</div> </div>
{autocompleteTagHintVisible && ( {autocompleteTagHintVisible && (
<AutocompleteTagHint viewControllerManager={viewControllerManager} closeOnBlur={closeOnBlur} /> <AutocompleteTagHint noteTagsController={noteTagsController} closeOnBlur={closeOnBlur} />
)} )}
</DisclosurePanel> </DisclosurePanel>
)} )}

View File

@@ -1,48 +1,48 @@
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
import { splitQueryInString } from '@/Utils/StringUtils' import { splitQueryInString } from '@/Utils/StringUtils'
import { SNTag } from '@standardnotes/snjs' import { SNTag } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FocusEventHandler, KeyboardEventHandler, useEffect, useRef } from 'react' import { FocusEventHandler, KeyboardEventHandler, useEffect, useRef } from 'react'
import Icon from '@/Components/Icon/Icon' import Icon from '@/Components/Icon/Icon'
import { NoteTagsController } from '@/Controllers/NoteTagsController'
type Props = { type Props = {
viewControllerManager: ViewControllerManager noteTagsController: NoteTagsController
tagResult: SNTag tagResult: SNTag
closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void
} }
const AutocompleteTagResult = ({ viewControllerManager, tagResult, closeOnBlur }: Props) => { const AutocompleteTagResult = ({ noteTagsController, tagResult, closeOnBlur }: Props) => {
const { autocompleteSearchQuery, autocompleteTagHintVisible, autocompleteTagResults, focusedTagResultUuid } = const { autocompleteSearchQuery, autocompleteTagHintVisible, autocompleteTagResults, focusedTagResultUuid } =
viewControllerManager.noteTagsController noteTagsController
const tagResultRef = useRef<HTMLButtonElement>(null) const tagResultRef = useRef<HTMLButtonElement>(null)
const title = tagResult.title const title = tagResult.title
const prefixTitle = viewControllerManager.noteTagsController.getPrefixTitle(tagResult) const prefixTitle = noteTagsController.getPrefixTitle(tagResult)
const onTagOptionClick = async (tag: SNTag) => { const onTagOptionClick = async (tag: SNTag) => {
await viewControllerManager.noteTagsController.addTagToActiveNote(tag) await noteTagsController.addTagToActiveNote(tag)
viewControllerManager.noteTagsController.clearAutocompleteSearch() noteTagsController.clearAutocompleteSearch()
viewControllerManager.noteTagsController.setAutocompleteInputFocused(true) noteTagsController.setAutocompleteInputFocused(true)
} }
const onKeyDown: KeyboardEventHandler = (event) => { const onKeyDown: KeyboardEventHandler = (event) => {
const tagResultIndex = viewControllerManager.noteTagsController.getTagIndex(tagResult, autocompleteTagResults) const tagResultIndex = noteTagsController.getTagIndex(tagResult, autocompleteTagResults)
switch (event.key) { switch (event.key) {
case 'ArrowUp': case 'ArrowUp':
event.preventDefault() event.preventDefault()
if (tagResultIndex === 0) { if (tagResultIndex === 0) {
viewControllerManager.noteTagsController.setAutocompleteInputFocused(true) noteTagsController.setAutocompleteInputFocused(true)
} else { } else {
viewControllerManager.noteTagsController.focusPreviousTagResult(tagResult) noteTagsController.focusPreviousTagResult(tagResult)
} }
break break
case 'ArrowDown': case 'ArrowDown':
event.preventDefault() event.preventDefault()
if (tagResultIndex === autocompleteTagResults.length - 1 && autocompleteTagHintVisible) { if (tagResultIndex === autocompleteTagResults.length - 1 && autocompleteTagHintVisible) {
viewControllerManager.noteTagsController.setAutocompleteTagHintFocused(true) noteTagsController.setAutocompleteTagHintFocused(true)
} else { } else {
viewControllerManager.noteTagsController.focusNextTagResult(tagResult) noteTagsController.focusNextTagResult(tagResult)
} }
break break
default: default:
@@ -51,20 +51,20 @@ const AutocompleteTagResult = ({ viewControllerManager, tagResult, closeOnBlur }
} }
const onFocus = () => { const onFocus = () => {
viewControllerManager.noteTagsController.setFocusedTagResultUuid(tagResult.uuid) noteTagsController.setFocusedTagResultUuid(tagResult.uuid)
} }
const onBlur: FocusEventHandler = (event) => { const onBlur: FocusEventHandler = (event) => {
closeOnBlur(event) closeOnBlur(event)
viewControllerManager.noteTagsController.setFocusedTagResultUuid(undefined) noteTagsController.setFocusedTagResultUuid(undefined)
} }
useEffect(() => { useEffect(() => {
if (focusedTagResultUuid === tagResult.uuid) { if (focusedTagResultUuid === tagResult.uuid) {
tagResultRef.current?.focus() tagResultRef.current?.focus()
viewControllerManager.noteTagsController.setFocusedTagResultUuid(undefined) noteTagsController.setFocusedTagResultUuid(undefined)
} }
}, [viewControllerManager, focusedTagResultUuid, tagResult]) }, [noteTagsController, focusedTagResultUuid, tagResult])
return ( return (
<button <button