feat: note tags button for mobile (#1641)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user