From fe2ce9f1e8b0c027c30bcd892606e914f71d4634 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Mon, 26 Sep 2022 02:58:55 +0530 Subject: [PATCH] feat: note tags button for mobile (#1641) --- .../Components/NoteTags/NoteTag.tsx | 40 +++-- .../Components/NoteTags/NoteTagsContainer.tsx | 30 ++-- .../Components/NoteTags/NoteTagsPanel.tsx | 170 ++++++++++++++++++ .../Components/NoteView/NoteView.tsx | 10 +- .../Popover/PositionedPopoverContent.tsx | 16 +- .../TagAutocomplete/AutocompleteTagHint.tsx | 32 ++-- .../TagAutocomplete/AutocompleteTagInput.tsx | 34 ++-- .../TagAutocomplete/AutocompleteTagResult.tsx | 34 ++-- 8 files changed, 273 insertions(+), 93 deletions(-) create mode 100644 packages/web/src/javascripts/Components/NoteTags/NoteTagsPanel.tsx diff --git a/packages/web/src/javascripts/Components/NoteTags/NoteTag.tsx b/packages/web/src/javascripts/Components/NoteTags/NoteTag.tsx index 22740ea61..6ce2c45e0 100644 --- a/packages/web/src/javascripts/Components/NoteTags/NoteTag.tsx +++ b/packages/web/src/javascripts/Components/NoteTags/NoteTag.tsx @@ -8,21 +8,23 @@ import { useRef, useState, } from 'react' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { SNTag } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider' import { AppPaneId } from '../ResponsivePane/AppPaneMetadata' +import { NoteTagsController } from '@/Controllers/NoteTagsController' +import { NavigationController } from '@/Controllers/Navigation/NavigationController' type Props = { - viewControllerManager: ViewControllerManager + noteTagsController: NoteTagsController + navigationController: NavigationController tag: SNTag } -const NoteTag = ({ viewControllerManager, tag }: Props) => { +const NoteTag = ({ noteTagsController, navigationController, tag }: Props) => { const { toggleAppPane } = useResponsiveAppPane() - const noteTags = viewControllerManager.noteTagsController + const noteTags = noteTagsController const { autocompleteInputFocused, focusedTagUuid, tags } = noteTags @@ -37,9 +39,9 @@ const NoteTag = ({ viewControllerManager, tag }: Props) => { const longTitle = noteTags.getLongTitle(tag) const deleteTag = useCallback(() => { - viewControllerManager.noteTagsController.focusPreviousTag(tag) - viewControllerManager.noteTagsController.removeTagFromActiveNote(tag).catch(console.error) - }, [viewControllerManager, tag]) + noteTagsController.focusPreviousTag(tag) + noteTagsController.removeTagFromActiveNote(tag).catch(console.error) + }, [noteTagsController, tag]) const onDeleteTagClick: MouseEventHandler = useCallback( (event) => { @@ -53,30 +55,30 @@ const NoteTag = ({ viewControllerManager, tag }: Props) => { async (event) => { if (tagClicked && event.target !== deleteTagRef.current) { setTagClicked(false) - await viewControllerManager.navigationController.setSelectedTag(tag) + await navigationController.setSelectedTag(tag) toggleAppPane(AppPaneId.Items) } else { setTagClicked(true) tagRef.current?.focus() } }, - [viewControllerManager, tagClicked, tag, toggleAppPane], + [tagClicked, navigationController, tag, toggleAppPane], ) const onFocus = useCallback(() => { - viewControllerManager.noteTagsController.setFocusedTagUuid(tag.uuid) + noteTagsController.setFocusedTagUuid(tag.uuid) setShowDeleteButton(true) - }, [viewControllerManager, tag]) + }, [noteTagsController, tag]) const onBlur: FocusEventHandler = useCallback( (event) => { const relatedTarget = event.relatedTarget as Node if (relatedTarget !== deleteTagRef.current) { - viewControllerManager.noteTagsController.setFocusedTagUuid(undefined) + noteTagsController.setFocusedTagUuid(undefined) setShowDeleteButton(false) } }, - [viewControllerManager], + [noteTagsController], ) const getTabIndex = useCallback(() => { @@ -91,33 +93,33 @@ const NoteTag = ({ viewControllerManager, tag }: Props) => { const onKeyDown: KeyboardEventHandler = useCallback( (event) => { - const tagIndex = viewControllerManager.noteTagsController.getTagIndex(tag, tags) + const tagIndex = noteTagsController.getTagIndex(tag, tags) switch (event.key) { case 'Backspace': deleteTag() break case 'ArrowLeft': - viewControllerManager.noteTagsController.focusPreviousTag(tag) + noteTagsController.focusPreviousTag(tag) break case 'ArrowRight': if (tagIndex === tags.length - 1) { - viewControllerManager.noteTagsController.setAutocompleteInputFocused(true) + noteTagsController.setAutocompleteInputFocused(true) } else { - viewControllerManager.noteTagsController.focusNextTag(tag) + noteTagsController.focusNextTag(tag) } break default: return } }, - [viewControllerManager, deleteTag, tag, tags], + [noteTagsController, deleteTag, tag, tags], ) useEffect(() => { if (focusedTagUuid === tag.uuid) { tagRef.current?.focus() } - }, [viewControllerManager, focusedTagUuid, tag]) + }, [noteTagsController, focusedTagUuid, tag]) return ( + ) : ( +
+ {longTitle} + +
+ ) +} + +const NoteTagsPanel = ({ + noteTagsController, + onClickPreprocessing, +}: { + noteTagsController: NoteTagsController + onClickPreprocessing?: () => Promise +}) => { + const isDesktopScreen = useMediaQuery(MediaQueryBreakpoints.md) + + const [isOpen, setIsOpen] = useState(false) + const buttonRef = useRef(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 = (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 ( + <> + + +
+ { + if (isOpen && node) { + node.focus() + } + }} + /> +
+
+ {visibleTagsList.map((tag) => ( + + ))} + {autocompleteTagHintVisible && ( + + )} +
+
+ + ) +} + +export default observer(NoteTagsPanel) diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index 1ce01253d..caf69c914 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -39,6 +39,7 @@ import IndicatorCircle from '../IndicatorCircle/IndicatorCircle' import { classNames } from '@/Utils/ConcatenateClassNames' import AutoresizingNoteViewTextarea from './AutoresizingTextarea' import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton' +import NoteTagsPanel from '../NoteTags/NoteTagsPanel' const MINIMUM_STATUS_DURATION = 400 const TEXTAREA_DEBOUNCE = 100 @@ -944,6 +945,10 @@ class NoteView extends PureComponent { )}
+ {
- + )} diff --git a/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx b/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx index ab0c00535..0d142de27 100644 --- a/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx +++ b/packages/web/src/javascripts/Components/Popover/PositionedPopoverContent.tsx @@ -71,17 +71,15 @@ const PositionedPopoverContent = ({ }} data-popover={id} > -
-
-
- -
- +
+
+
- {children} +
+
{children}
) diff --git a/packages/web/src/javascripts/Components/TagAutocomplete/AutocompleteTagHint.tsx b/packages/web/src/javascripts/Components/TagAutocomplete/AutocompleteTagHint.tsx index dae670cbd..8f7f95b10 100644 --- a/packages/web/src/javascripts/Components/TagAutocomplete/AutocompleteTagHint.tsx +++ b/packages/web/src/javascripts/Components/TagAutocomplete/AutocompleteTagHint.tsx @@ -1,36 +1,36 @@ -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' +import { NoteTagsController } from '@/Controllers/NoteTagsController' import { observer } from 'mobx-react-lite' import { useRef, useEffect, useCallback, FocusEventHandler, KeyboardEventHandler } from 'react' import Icon from '@/Components/Icon/Icon' import HorizontalSeparator from '../Shared/HorizontalSeparator' type Props = { - viewControllerManager: ViewControllerManager + noteTagsController: NoteTagsController closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void } -const AutocompleteTagHint = ({ viewControllerManager, closeOnBlur }: Props) => { - const { autocompleteTagHintFocused } = viewControllerManager.noteTagsController +const AutocompleteTagHint = ({ noteTagsController, closeOnBlur }: Props) => { + const { autocompleteTagHintFocused } = noteTagsController const hintRef = useRef(null) - const { autocompleteSearchQuery, autocompleteTagResults } = viewControllerManager.noteTagsController + const { autocompleteSearchQuery, autocompleteTagResults } = noteTagsController const onTagHintClick = useCallback(async () => { - await viewControllerManager.noteTagsController.createAndAddNewTag() - viewControllerManager.noteTagsController.setAutocompleteInputFocused(true) - }, [viewControllerManager]) + await noteTagsController.createAndAddNewTag() + noteTagsController.setAutocompleteInputFocused(true) + }, [noteTagsController]) const onFocus = useCallback(() => { - viewControllerManager.noteTagsController.setAutocompleteTagHintFocused(true) - }, [viewControllerManager]) + noteTagsController.setAutocompleteTagHintFocused(true) + }, [noteTagsController]) const onBlur: FocusEventHandler = useCallback( (event) => { closeOnBlur(event) - viewControllerManager.noteTagsController.setAutocompleteTagHintFocused(false) + noteTagsController.setAutocompleteTagHintFocused(false) }, - [viewControllerManager, closeOnBlur], + [noteTagsController, closeOnBlur], ) const onKeyDown: KeyboardEventHandler = useCallback( @@ -38,20 +38,20 @@ const AutocompleteTagHint = ({ viewControllerManager, closeOnBlur }: Props) => { if (event.key === 'ArrowUp') { if (autocompleteTagResults.length > 0) { const lastTagResult = autocompleteTagResults[autocompleteTagResults.length - 1] - viewControllerManager.noteTagsController.setFocusedTagResultUuid(lastTagResult.uuid) + noteTagsController.setFocusedTagResultUuid(lastTagResult.uuid) } else { - viewControllerManager.noteTagsController.setAutocompleteInputFocused(true) + noteTagsController.setAutocompleteInputFocused(true) } } }, - [viewControllerManager, autocompleteTagResults], + [noteTagsController, autocompleteTagResults], ) useEffect(() => { if (autocompleteTagHintFocused) { hintRef.current?.focus() } - }, [viewControllerManager, autocompleteTagHintFocused]) + }, [noteTagsController, autocompleteTagHintFocused]) return ( <> diff --git a/packages/web/src/javascripts/Components/TagAutocomplete/AutocompleteTagInput.tsx b/packages/web/src/javascripts/Components/TagAutocomplete/AutocompleteTagInput.tsx index c70296089..1d5321f6d 100644 --- a/packages/web/src/javascripts/Components/TagAutocomplete/AutocompleteTagInput.tsx +++ b/packages/web/src/javascripts/Components/TagAutocomplete/AutocompleteTagInput.tsx @@ -9,19 +9,19 @@ import { } from 'react' import { Disclosure, DisclosurePanel } from '@reach/disclosure' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import AutocompleteTagResult from './AutocompleteTagResult' import AutocompleteTagHint from './AutocompleteTagHint' import { observer } from 'mobx-react-lite' import { SNTag } from '@standardnotes/snjs' import { classNames } from '@/Utils/ConcatenateClassNames' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' +import { NoteTagsController } from '@/Controllers/NoteTagsController' type Props = { - viewControllerManager: ViewControllerManager + noteTagsController: NoteTagsController } -const AutocompleteTagInput = ({ viewControllerManager }: Props) => { +const AutocompleteTagInput = ({ noteTagsController }: Props) => { const { autocompleteInputFocused, autocompleteSearchQuery, @@ -29,7 +29,7 @@ const AutocompleteTagInput = ({ viewControllerManager }: Props) => { autocompleteTagResults, tags, tagsContainerMaxWidth, - } = viewControllerManager.noteTagsController + } = noteTagsController const [dropdownVisible, setDropdownVisible] = useState(false) const [dropdownMaxHeight, setDropdownMaxHeight] = useState('auto') @@ -39,7 +39,7 @@ const AutocompleteTagInput = ({ viewControllerManager }: Props) => { const [closeOnBlur] = useCloseOnBlur(containerRef, (visible: boolean) => { setDropdownVisible(visible) - viewControllerManager.noteTagsController.clearAutocompleteSearch() + noteTagsController.clearAutocompleteSearch() }) const showDropdown = () => { @@ -55,17 +55,17 @@ const AutocompleteTagInput = ({ viewControllerManager }: Props) => { const query = event.target.value if (query === '') { - viewControllerManager.noteTagsController.clearAutocompleteSearch() + noteTagsController.clearAutocompleteSearch() } else { - viewControllerManager.noteTagsController.setAutocompleteSearchQuery(query) - viewControllerManager.noteTagsController.searchActiveNoteAutocompleteTags() + noteTagsController.setAutocompleteSearchQuery(query) + noteTagsController.searchActiveNoteAutocompleteTags() } } const onFormSubmit: FormEventHandler = async (event) => { event.preventDefault() if (autocompleteSearchQuery !== '') { - await viewControllerManager.noteTagsController.createAndAddNewTag() + await noteTagsController.createAndAddNewTag() } } @@ -74,15 +74,15 @@ const AutocompleteTagInput = ({ viewControllerManager }: Props) => { case 'Backspace': case 'ArrowLeft': if (autocompleteSearchQuery === '' && tags.length > 0) { - viewControllerManager.noteTagsController.setFocusedTagUuid(tags[tags.length - 1].uuid) + noteTagsController.setFocusedTagUuid(tags[tags.length - 1].uuid) } break case 'ArrowDown': event.preventDefault() if (autocompleteTagResults.length > 0) { - viewControllerManager.noteTagsController.setFocusedTagResultUuid(autocompleteTagResults[0].uuid) + noteTagsController.setFocusedTagResultUuid(autocompleteTagResults[0].uuid) } else if (autocompleteTagHintVisible) { - viewControllerManager.noteTagsController.setAutocompleteTagHintFocused(true) + noteTagsController.setAutocompleteTagHintFocused(true) } break default: @@ -92,19 +92,19 @@ const AutocompleteTagInput = ({ viewControllerManager }: Props) => { const onFocus = () => { showDropdown() - viewControllerManager.noteTagsController.setAutocompleteInputFocused(true) + noteTagsController.setAutocompleteInputFocused(true) } const onBlur: FocusEventHandler = (event) => { closeOnBlur(event) - viewControllerManager.noteTagsController.setAutocompleteInputFocused(false) + noteTagsController.setAutocompleteInputFocused(false) } useEffect(() => { if (autocompleteInputFocused) { inputRef.current?.focus() } - }, [viewControllerManager, autocompleteInputFocused]) + }, [autocompleteInputFocused]) return (
@@ -140,14 +140,14 @@ const AutocompleteTagInput = ({ viewControllerManager }: Props) => { {autocompleteTagResults.map((tagResult: SNTag) => ( ))}
{autocompleteTagHintVisible && ( - + )} )} diff --git a/packages/web/src/javascripts/Components/TagAutocomplete/AutocompleteTagResult.tsx b/packages/web/src/javascripts/Components/TagAutocomplete/AutocompleteTagResult.tsx index a94ccbf0c..c97b37338 100644 --- a/packages/web/src/javascripts/Components/TagAutocomplete/AutocompleteTagResult.tsx +++ b/packages/web/src/javascripts/Components/TagAutocomplete/AutocompleteTagResult.tsx @@ -1,48 +1,48 @@ -import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { splitQueryInString } from '@/Utils/StringUtils' import { SNTag } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FocusEventHandler, KeyboardEventHandler, useEffect, useRef } from 'react' import Icon from '@/Components/Icon/Icon' +import { NoteTagsController } from '@/Controllers/NoteTagsController' type Props = { - viewControllerManager: ViewControllerManager + noteTagsController: NoteTagsController tagResult: SNTag closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void } -const AutocompleteTagResult = ({ viewControllerManager, tagResult, closeOnBlur }: Props) => { +const AutocompleteTagResult = ({ noteTagsController, tagResult, closeOnBlur }: Props) => { const { autocompleteSearchQuery, autocompleteTagHintVisible, autocompleteTagResults, focusedTagResultUuid } = - viewControllerManager.noteTagsController + noteTagsController const tagResultRef = useRef(null) const title = tagResult.title - const prefixTitle = viewControllerManager.noteTagsController.getPrefixTitle(tagResult) + const prefixTitle = noteTagsController.getPrefixTitle(tagResult) const onTagOptionClick = async (tag: SNTag) => { - await viewControllerManager.noteTagsController.addTagToActiveNote(tag) - viewControllerManager.noteTagsController.clearAutocompleteSearch() - viewControllerManager.noteTagsController.setAutocompleteInputFocused(true) + await noteTagsController.addTagToActiveNote(tag) + noteTagsController.clearAutocompleteSearch() + noteTagsController.setAutocompleteInputFocused(true) } const onKeyDown: KeyboardEventHandler = (event) => { - const tagResultIndex = viewControllerManager.noteTagsController.getTagIndex(tagResult, autocompleteTagResults) + const tagResultIndex = noteTagsController.getTagIndex(tagResult, autocompleteTagResults) switch (event.key) { case 'ArrowUp': event.preventDefault() if (tagResultIndex === 0) { - viewControllerManager.noteTagsController.setAutocompleteInputFocused(true) + noteTagsController.setAutocompleteInputFocused(true) } else { - viewControllerManager.noteTagsController.focusPreviousTagResult(tagResult) + noteTagsController.focusPreviousTagResult(tagResult) } break case 'ArrowDown': event.preventDefault() if (tagResultIndex === autocompleteTagResults.length - 1 && autocompleteTagHintVisible) { - viewControllerManager.noteTagsController.setAutocompleteTagHintFocused(true) + noteTagsController.setAutocompleteTagHintFocused(true) } else { - viewControllerManager.noteTagsController.focusNextTagResult(tagResult) + noteTagsController.focusNextTagResult(tagResult) } break default: @@ -51,20 +51,20 @@ const AutocompleteTagResult = ({ viewControllerManager, tagResult, closeOnBlur } } const onFocus = () => { - viewControllerManager.noteTagsController.setFocusedTagResultUuid(tagResult.uuid) + noteTagsController.setFocusedTagResultUuid(tagResult.uuid) } const onBlur: FocusEventHandler = (event) => { closeOnBlur(event) - viewControllerManager.noteTagsController.setFocusedTagResultUuid(undefined) + noteTagsController.setFocusedTagResultUuid(undefined) } useEffect(() => { if (focusedTagResultUuid === tagResult.uuid) { tagResultRef.current?.focus() - viewControllerManager.noteTagsController.setFocusedTagResultUuid(undefined) + noteTagsController.setFocusedTagResultUuid(undefined) } - }, [viewControllerManager, focusedTagResultUuid, tagResult]) + }, [noteTagsController, focusedTagResultUuid, tagResult]) return (