From 5ee2d88db20d64b4b5919a6861a06c364bc6bd37 Mon Sep 17 00:00:00 2001 From: Xu Quan Date: Fri, 11 Jul 2025 23:02:42 +0800 Subject: [PATCH] refactor: put duplicate logic of folder/tag/file name component into EditableName --- src/components/EditableName/index.tsx | 168 +++++++++++++++++++++ src/components/File/Content.tsx | 44 ++---- src/components/File/ContentInner.tsx | 22 +-- src/components/File/Name.tsx | 79 ++++------ src/components/Folder/Content.tsx | 79 ++++------ src/components/Folder/ContentInner.tsx | 27 ---- src/components/Folder/Name.tsx | 91 ++++------- src/components/ScrollInToViewContainer.tsx | 32 ++++ src/components/Tag/Content.tsx | 76 +++++----- src/components/Tag/Name.tsx | 39 +++++ src/components/Tag/index.tsx | 74 +++------ src/components/TogglableContainer.tsx | 62 ++++++++ 12 files changed, 472 insertions(+), 321 deletions(-) create mode 100644 src/components/EditableName/index.tsx delete mode 100644 src/components/Folder/ContentInner.tsx create mode 100644 src/components/ScrollInToViewContainer.tsx create mode 100644 src/components/Tag/Name.tsx create mode 100644 src/components/TogglableContainer.tsx diff --git a/src/components/EditableName/index.tsx b/src/components/EditableName/index.tsx new file mode 100644 index 0000000..ee46dc6 --- /dev/null +++ b/src/components/EditableName/index.tsx @@ -0,0 +1,168 @@ +import classNames from "classnames"; +import { + forwardRef, + RefObject, + useEffect, + useImperativeHandle, + useRef, + useState, +} from "react"; + +import { Noop } from "src/utils"; + +export type NameRef = { + isFocusing: boolean; + setIsFocusing: (isFocusing: boolean) => void; + onStartEditingName: Noop; +}; + +type Props = { + defaultName: string; + onSaveName: (name: string) => Promise; + contentRef: RefObject; + isFocused: boolean; + className?: string; + boldName?: boolean; +}; +const EditableName = forwardRef( + ( + { + defaultName, + onSaveName, + isFocused, + className = "", + boldName = false, + contentRef, + }: Props, + ref + ) => { + const eleRef = useRef(null); + const inputRef = useRef(null); + + const [isEditing, setIsEditing] = useState(false); + const [isFocusing, setIsFocusing] = useState(false); + const [name, setName] = useState(defaultName); + + useEffect(() => { + setIsFocusing(false); + }, [isFocused]); + + useEffect(() => { + if (isFocused) { + setTimeout(() => { + setIsFocusing(true); + }, 100); + } + }, []); + + const selectFileNameText = () => { + const inputEle = inputRef.current; + if (inputEle) { + inputEle.focus(); + inputEle.setSelectionRange(0, inputEle.value.length); + } + }; + + const onStartEditingName = () => { + setIsEditing(true); + setName(name); + setTimeout(() => { + selectFileNameText(); + }, 100); + }; + + const onCancelEditing = () => { + setIsEditing(false); + setName(defaultName); + }; + + const onSaveNewName = async () => { + try { + await onSaveName(name.trim()); + setIsEditing(false); + } catch (error) { + onCancelEditing(); + throw error; + } + }; + + const onKeyDownInInput = ( + event: React.KeyboardEvent + ): void => { + if (event.key === "Escape") { + event.preventDefault(); + onCancelEditing(); + return; + } + + if (event.key === "Enter") { + event.preventDefault(); + onSaveNewName(); + } + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Enter" && isFocusing) { + onStartEditingName(); + } + }; + + const onClickOutside = (event: MouseEvent) => { + const { current: input } = inputRef; + const { current: content } = contentRef; + const node = event.target as Node; + if (input && !input.contains(node)) { + if (isEditing) { + onSaveNewName(); + } + } + if (content && !content.contains(node)) { + setIsFocusing(false); + } + }; + + useEffect(() => { + window.addEventListener("keydown", onKeyDown); + window.addEventListener("mousedown", onClickOutside); + return () => { + window.removeEventListener("keydown", onKeyDown); + window.removeEventListener("mousedown", onClickOutside); + }; + }, [isEditing, name, isFocusing, contentRef]); + + useImperativeHandle( + ref, + () => ({ + isFocusing, + setIsFocusing, + onStartEditingName, + }), + [isFocusing, setIsFocusing, onStartEditingName] + ); + + const renderInput = () => ( + setName(e.target.value)} + /> + ); + + const renderNameDisplay = () => ( +
+ {name} +
+ ); + + return isEditing ? renderInput() : renderNameDisplay(); + } +); + +export default EditableName; diff --git a/src/components/File/Content.tsx b/src/components/File/Content.tsx index 489c98c..f1b9d51 100644 --- a/src/components/File/Content.tsx +++ b/src/components/File/Content.tsx @@ -1,6 +1,6 @@ import classNames from "classnames"; import { Menu, TFile } from "obsidian"; -import { useEffect, useRef } from "react"; +import { useRef } from "react"; import { useShallow } from "zustand/react/shallow"; import { useExplorer } from "src/hooks/useExplorer"; @@ -19,7 +19,9 @@ import { triggerMenu, } from "src/utils"; +import { NameRef } from "../EditableName"; import { FolderListModal } from "../FolderListModal"; +import ScrollInToViewContainer from "../ScrollInToViewContainer"; import FileContentInner from "./ContentInner"; @@ -36,7 +38,6 @@ const FileContent = ({ file }: FileProps) => { const { language, settings } = plugin; const { - focusedFile, selectFileAndOpen, createFileWithDefaultName, duplicateFile, @@ -44,9 +45,9 @@ const FileContent = ({ file }: FileProps) => { pinFile, unpinFile, trashFile, + isFocusedFile, } = useExplorerStore( useShallow((store: ExplorerStore) => ({ - focusedFile: store.focusedFile, selectFileAndOpen: store.selectFileAndOpen, createFileWithDefaultName: store.createFileWithDefaultName, duplicateFile: store.duplicateFile, @@ -54,6 +55,9 @@ const FileContent = ({ file }: FileProps) => { pinFile: store.pinFile, unpinFile: store.unpinFile, trashFile: store.trashFile, + isFocusedFile: store.isFocusedFile, + + focusedFile: store.focusedFile, })) ); @@ -61,19 +65,9 @@ const FileContent = ({ file }: FileProps) => { settings.showFileItemDivider ); - const isFocused = focusedFile?.path === file.path; - const fileRef = useRef(null); - const innerContentRef = useRef(null); + const nameRef = useRef(null); - useEffect(() => { - if (focusedFile?.path !== file.path) return; - setTimeout(() => { - fileRef.current?.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - }, 100); - }, [focusedFile]); + const isFocused = isFocusedFile(file); const addPinInMenu = (menu: Menu) => { const isPinned = isFilePinned(file); @@ -135,10 +129,7 @@ const FileContent = ({ file }: FileProps) => { addMoveMenuItem(menu, onShowTargetFoldersList); menu.addSeparator(); - addRenameMenuItem( - menu, - innerContentRef.current?.onStartEditingName ?? noop - ); + addRenameMenuItem(menu, nameRef.current?.onStartEditingName ?? noop); addDeleteMenuItem(menu, () => trashFile(file)); triggerMenu(plugin, menu, "file-context-menu")(e); @@ -159,24 +150,19 @@ const FileContent = ({ file }: FileProps) => { openFileInNewTab(file); } else if (isFocused) { e.stopPropagation(); - fileRef.current?.focus(); - innerContentRef.current?.setIsFocusing(true); + nameRef.current?.setIsFocusing(true); } }; return ( -
- -
+ + ); }; diff --git a/src/components/File/ContentInner.tsx b/src/components/File/ContentInner.tsx index 5b5c62a..796f216 100644 --- a/src/components/File/ContentInner.tsx +++ b/src/components/File/ContentInner.tsx @@ -1,27 +1,25 @@ import classNames from "classnames"; import { TFile } from "obsidian"; -import { forwardRef, RefObject } from "react"; +import { forwardRef, RefObject, useRef } from "react"; import { useExplorer } from "src/hooks/useExplorer"; import { useFileItemSpacing, useShowFileDetail, } from "src/hooks/useSettingsHandler"; +import { FILE_ITEM_SPACING } from "src/settings"; + +import { NameRef } from "../EditableName"; -import { FileInnerContentRef } from "./Content"; import FileDetail from "./Detail"; import FileExtension from "./Extension"; import FileName from "./Name"; -export type Props = { +type Props = { file: TFile; - fileRef: RefObject; }; const FileContentInner = forwardRef( - ( - { file, fileRef }: Props, - ref: React.ForwardedRef - ) => { + ({ file }: Props, ref: RefObject) => { const { plugin } = useExplorer(); const { settings } = plugin; @@ -30,6 +28,8 @@ const FileContentInner = forwardRef( const { showFileDetail } = useShowFileDetail(showDetail); const { fileItemSpacing } = useFileItemSpacing(spacing); + const contentRef = useRef(null) + const maybeRenderFileDetail = () => { if (!showFileDetail) return null; return ; @@ -40,16 +40,16 @@ const FileContentInner = forwardRef( "ffs__file-content-header tree-item-inner nav-file-title-content", { "ffs__file-content-header--comfortable": - fileItemSpacing === "Comfortable", + fileItemSpacing === FILE_ITEM_SPACING.COMFORTABLE, "ffs__file-content-header--with-detail": showFileDetail, } ); }; return ( -
+
- +
{maybeRenderFileDetail()} diff --git a/src/components/File/Name.tsx b/src/components/File/Name.tsx index 762ce56..1decbee 100644 --- a/src/components/File/Name.tsx +++ b/src/components/File/Name.tsx @@ -1,79 +1,56 @@ -import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; +import { TFile } from "obsidian"; +import { forwardRef, RefObject, useEffect } from "react"; import { useShallow } from "zustand/react/shallow"; import { useExplorer } from "src/hooks/useExplorer"; -import useRenderEditableName from "src/hooks/useRenderEditableName"; import { useBoldFileTitle } from "src/hooks/useSettingsHandler"; import { ExplorerStore } from "src/store"; import { UNTITLED_NAME } from "src/utils"; -import { FileInnerContentRef } from "./Content"; -import { Props } from "./ContentInner"; +import EditableName, { NameRef } from "../EditableName"; +type Props = { + file: TFile + contentRef: RefObject +} const FileName = forwardRef( - ( - { file, fileRef }: Props, - ref: React.ForwardedRef - ) => { + ({ file, contentRef }: Props, ref: RefObject) => { const { useExplorerStore, plugin } = useExplorer(); const { settings } = plugin; - const { renameFile } = useExplorerStore( + const { renameFile, isFocusedFile } = useExplorerStore( useShallow((store: ExplorerStore) => ({ renameFile: store.renameFile, + isFocusedFile: store.isFocusedFile, })) ); const { boldFileTitle } = useBoldFileTitle(settings.boldFileTitle); - useImperativeHandle(ref, () => ({ - setIsFocusing, - onStartEditingName, - })); - - const onSaveName = (name: string) => renameFile(file, name); - - const { renderEditableName: renderFileName, onStartEditingName } = - useRenderEditableName(file.basename, onSaveName, { - className: "ffs__file-name", - bold: boldFileTitle, - }); - - const [isFocusing, setIsFocusing] = useState(false); - - const onKeyDown = (event: KeyboardEvent) => { - if (event.key === "Enter" && isFocusing) { - onStartEditingName(); - } - }; - - const onClickOutside = (event: MouseEvent) => { - if ( - fileRef.current && - !fileRef.current.contains(event.target as Node) - ) { - setIsFocusing(false); - } - }; - - useEffect(() => { - window.addEventListener("keydown", onKeyDown); - window.addEventListener("mousedown", onClickOutside); - return () => { - window.removeEventListener("keydown", onKeyDown); - window.removeEventListener("mousedown", onClickOutside); - }; - }, [isFocusing]); - useEffect(() => { const now = Date.now(); const fileCreateTime = file.stat.ctime; - if (now - fileCreateTime < 3000 && file.name.includes(UNTITLED_NAME)) { - onStartEditingName(); + if ( + now - fileCreateTime < 3000 && + file.name.includes(UNTITLED_NAME) + ) { + ref.current?.onStartEditingName(); } }, []); - return renderFileName(); + return ( + + await renameFile(file, name) + } + className="ffs__file-name" + boldName={boldFileTitle} + isFocused={isFocusedFile(file)} + /> + ); } ); diff --git a/src/components/Folder/Content.tsx b/src/components/Folder/Content.tsx index 34f01cd..bcfd6e2 100644 --- a/src/components/Folder/Content.tsx +++ b/src/components/Folder/Content.tsx @@ -1,12 +1,9 @@ -import classNames from "classnames"; import { Menu, TFolder } from "obsidian"; -import { MouseEvent, useEffect, useRef } from "react"; +import { useEffect, useRef } from "react"; import { useShallow } from "zustand/react/shallow"; import { useExplorer } from "src/hooks/useExplorer"; -import { useExpandNodeByClick } from "src/hooks/useSettingsHandler"; import { FOLDER_OPERATION_COPY } from "src/locales"; -import { EXPAND_NODE_ON_CLICK } from "src/settings"; import { ExplorerStore } from "src/store"; import { addCreateFileMenuItem, @@ -16,19 +13,16 @@ import { addPinMenuItem, addRenameMenuItem, noop, - Noop, triggerMenu, } from "src/utils"; +import { NameRef } from "../EditableName"; import { FolderListModal } from "../FolderListModal"; +import TogglableContainer from "../TogglableContainer"; -import FolderContentInner from "./ContentInner"; - -export type FolderNameRef = { - isFocusing: boolean; - setIsFocusing: (isFocusing: boolean) => void; - onStartEditingName: Noop; -}; +import FolderFilesCount from "./FilesCount"; +import FolderIcon from "./Icon"; +import FolderName from "./Name"; export type FolderProps = { folder: TFolder; @@ -36,7 +30,7 @@ export type FolderProps = { type Props = FolderProps; const FolderContent = ({ folder }: Props) => { const { useExplorerStore, plugin } = useExplorer(); - const { language, settings } = plugin; + const { language } = plugin; const { focusedFolder, @@ -48,6 +42,7 @@ const FolderContent = ({ folder }: Props) => { isFolderPinned, trashFolder, toggleFolder, + isFocusedFolder, } = useExplorerStore( useShallow((store: ExplorerStore) => ({ focusedFolder: store.focusedFolder, @@ -59,27 +54,14 @@ const FolderContent = ({ folder }: Props) => { isFolderPinned: store.isFolderPinned, trashFolder: store.trashFolder, toggleFolder: store.toggleFolder, + isFocusedFolder: store.isFocusedFolder, })) ); - const { expandNodeByClick } = useExpandNodeByClick( - settings.expandNodeOnClick - ); + const nameRef = useRef(null); + const contentRef = useRef(null); - const folderRef = useRef(null); - const nameRef = useRef(null); - - useEffect(() => { - nameRef.current?.setIsFocusing(false) - - if (focusedFolder?.path !== folder.path) return; - setTimeout(() => { - folderRef.current?.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - }, 100); - }, [focusedFolder]); + const isFocused = isFocusedFolder(folder); const addPinInMenu = (menu: Menu) => { const isPinned = isFolderPinned(folder); @@ -133,30 +115,25 @@ const FolderContent = ({ folder }: Props) => { triggerMenu(plugin, menu, "folder-context-menu")(e); }; - const onClickFolderContent = async (e: MouseEvent) => { - e.stopPropagation() - e.preventDefault() - await changeFocusedFolder(folder); - if (expandNodeByClick === EXPAND_NODE_ON_CLICK.LABEL) { - toggleFolder(folder); - } else if (expandNodeByClick === EXPAND_NODE_ON_CLICK.SELECTED_LABEL) { - folderRef.current?.focus(); - if (nameRef.current?.isFocusing) { - toggleFolder(folder); - } else { - nameRef.current?.setIsFocusing(true); - } - } - }; - return ( -
await changeFocusedFolder(folder)} + onToggle={() => toggleFolder(folder)} + className="ffs__folder" onContextMenu={onShowContextMenu} - onClick={onClickFolderContent} > - -
+
+ + + +
+ ); }; diff --git a/src/components/Folder/ContentInner.tsx b/src/components/Folder/ContentInner.tsx deleted file mode 100644 index ee940aa..0000000 --- a/src/components/Folder/ContentInner.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { TFolder } from "obsidian"; -import { ForwardedRef, forwardRef, Ref } from "react"; - -import { FolderNameRef } from "./Content"; -import FolderFilesCount from "./FilesCount"; -import FolderIcon from "./Icon"; -import FolderName from "./Name"; - -export type FolderProps = { - folder: TFolder; - nameRef: Ref -}; -type Props = FolderProps; -const FolderContentInner = forwardRef( - ({ folder, nameRef }: Props, ref: ForwardedRef) => { - - return ( -
- - - -
- ); - } -); - -export default FolderContentInner; diff --git a/src/components/Folder/Name.tsx b/src/components/Folder/Name.tsx index 0a4fe44..71e25d9 100644 --- a/src/components/Folder/Name.tsx +++ b/src/components/Folder/Name.tsx @@ -1,84 +1,53 @@ import { TFolder } from "obsidian"; -import { - ForwardedRef, - forwardRef, - useEffect, - useImperativeHandle, - useRef, - useState, -} from "react"; +import { forwardRef, RefObject, useEffect } from "react"; import { useShallow } from "zustand/react/shallow"; import { useExplorer } from "src/hooks/useExplorer"; -import useRenderEditableName from "src/hooks/useRenderEditableName"; import { ExplorerStore } from "src/store"; -import { FolderNameRef } from "./Content"; +import EditableName, { NameRef } from "../EditableName"; export type FolderProps = { folder: TFolder; + contentRef: RefObject; }; type Props = FolderProps; const FolderName = forwardRef( - ({ folder }: Props, ref: ForwardedRef) => { + ({ folder, contentRef }: Props, ref: RefObject) => { const { useExplorerStore } = useExplorer(); - const { renameFolder, getNameOfFolder, isLastCreatedFolder } = - useExplorerStore( - useShallow((store: ExplorerStore) => ({ - renameFolder: store.renameFolder, - getNameOfFolder: store.getNameOfFolder, - isLastCreatedFolder: store.isLastCreatedFolder, - })) - ); - - const onSaveName = (name: string) => renameFolder(folder, name); - const folderName = getNameOfFolder(folder); - const { renderEditableName: renderFolderName, onStartEditingName } = - useRenderEditableName(folderName, onSaveName, { - className: "ffs__folder-name", - }); - - const folderRef = useRef(null); - const [isFocusing, setIsFocusing] = useState(false); - - useImperativeHandle(ref, () => ({ - setIsFocusing, - onStartEditingName, - isFocusing, - })); - - const onClickOutside = (event: MouseEvent) => { - if ( - folderRef.current && - !folderRef.current.contains(event.target as Node) - ) { - setIsFocusing(false); - } - }; - - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter" && isFocusing) { - onStartEditingName(); - } - }; - - useEffect(() => { - window.addEventListener("keydown", onKeyDown); - window.addEventListener("mousedown", onClickOutside); - return () => { - window.removeEventListener("keydown", onKeyDown); - window.removeEventListener("mousedown", onClickOutside); - }; - }, [isFocusing]); + const { + renameFolder, + getNameOfFolder, + isLastCreatedFolder, + isFocusedFolder, + } = useExplorerStore( + useShallow((store: ExplorerStore) => ({ + renameFolder: store.renameFolder, + getNameOfFolder: store.getNameOfFolder, + isLastCreatedFolder: store.isLastCreatedFolder, + isFocusedFolder: store.isFocusedFolder, + })) + ); useEffect(() => { if (isLastCreatedFolder(folder)) { - onStartEditingName(); + ref.current?.onStartEditingName(); } }, []); - return renderFolderName(); + return ( + + await renameFolder(folder, name) + } + /> + ); } ); diff --git a/src/components/ScrollInToViewContainer.tsx b/src/components/ScrollInToViewContainer.tsx new file mode 100644 index 0000000..ccafac0 --- /dev/null +++ b/src/components/ScrollInToViewContainer.tsx @@ -0,0 +1,32 @@ +import { HTMLAttributes, ReactNode, useEffect, useRef } from "react"; + +type Props = { + needToScroll: boolean; + children: ReactNode; +} & HTMLAttributes; +const ScrollInToViewContainer = ({ + needToScroll, + children, + ...props +}: Props) => { + const eleRef = useRef(null); + + useEffect(() => { + if (needToScroll) { + setTimeout(() => { + eleRef.current?.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + }, 100); + } + }, [needToScroll]); + + return ( +
+ {children} +
+ ); +}; + +export default ScrollInToViewContainer; diff --git a/src/components/Tag/Content.tsx b/src/components/Tag/Content.tsx index 621f2cf..2a3da74 100644 --- a/src/components/Tag/Content.tsx +++ b/src/components/Tag/Content.tsx @@ -1,13 +1,21 @@ -import classNames from "classnames"; import { Menu } from "obsidian"; +import { useRef } from "react"; import { useShallow } from "zustand/react/shallow"; import { useExplorer } from "src/hooks/useExplorer"; -import useRenderEditableName from "src/hooks/useRenderEditableName"; import { TagNode } from "src/store/tag"; -import { addPinMenuItem, addRenameMenuItem, triggerMenu } from "src/utils"; +import { + addPinMenuItem, + addRenameMenuItem, + noop, + triggerMenu, +} from "src/utils"; + +import { NameRef } from "../EditableName"; +import TogglableContainer from "../TogglableContainer"; import TagFilesCount from "./FilesCount"; +import TagName from "./Name"; import TagIcon from "./TagIcon"; type Props = { @@ -16,29 +24,29 @@ type Props = { const TagContent = ({ tag }: Props) => { const { plugin, useExplorerStore } = useExplorer(); - const { renameTag, focusedTag, isTagPinned, pinTag, unpinTag } = - useExplorerStore( - useShallow((store) => ({ - renameTag: store.renameTag, - focusedTag: store.focusedTag, - isTagPinned: store.isTagPinned, - pinTag: store.pinTag, - unpinTag: store.unpinTag, - })) - ); + const { + focusedTag, + isTagPinned, + pinTag, + unpinTag, + changeFocusedTag, + toggleTag, + } = useExplorerStore( + useShallow((store) => ({ + focusedTag: store.focusedTag, + isTagPinned: store.isTagPinned, + pinTag: store.pinTag, + unpinTag: store.unpinTag, + changeFocusedTag: store.changeFocusedTag, + toggleTag: store.toggleTag, + })) + ); + + const nameRef = useRef(null); + const contentRef = useRef(null); const isFocused = tag.fullPath == focusedTag?.fullPath; - const onSaveName = (name: string) => renameTag(tag, name); - const { - renderEditableName: renderTagName, - onStartEditingName, - setIsFocusing, - contentRef: tagRef, - } = useRenderEditableName(tag.name, onSaveName, { - className: "ffs__tag-name", - }); - const addPinInMenu = (menu: Menu) => { const isPinned = isTagPinned(tag); addPinMenuItem(menu, { @@ -56,32 +64,30 @@ const TagContent = ({ tag }: Props) => { addPinInMenu(menu); menu.addSeparator(); - addRenameMenuItem(menu, onStartEditingName); + addRenameMenuItem(menu, nameRef.current?.onStartEditingName ?? noop); triggerMenu(plugin, menu, "tag-context-menu")(e); }; const renderTitleContent = () => ( -
+
- {renderTagName()} +
); return ( -
await changeFocusedTag(tag)} + onToggle={() => toggleTag(tag)} + className="ffs__tag" onContextMenu={onShowContextMenu} - onClick={(e) => { - if (isFocused) { - tagRef.current?.focus(); - setIsFocusing(true); - } - }} > {renderTitleContent()} -
+ ); }; diff --git a/src/components/Tag/Name.tsx b/src/components/Tag/Name.tsx new file mode 100644 index 0000000..c21d868 --- /dev/null +++ b/src/components/Tag/Name.tsx @@ -0,0 +1,39 @@ +import { forwardRef, RefObject } from "react"; +import { useShallow } from "zustand/react/shallow"; + +import { useExplorer } from "src/hooks/useExplorer"; +import { ExplorerStore } from "src/store"; +import { TagNode } from "src/store/tag"; + +import EditableName, { NameRef } from "../EditableName"; + +export type FolderProps = { + tag: TagNode; + contentRef: RefObject; +}; +type Props = FolderProps; +const TagName = forwardRef( + ({ tag, contentRef }: Props, ref: RefObject) => { + const { useExplorerStore } = useExplorer(); + + const { renameTag, isFocusedTag } = useExplorerStore( + useShallow((store: ExplorerStore) => ({ + renameTag: store.renameTag, + isFocusedTag: store.isFocusedTag, + })) + ); + + return ( + await renameTag(tag, name)} + /> + ); + } +); + +export default TagName; diff --git a/src/components/Tag/index.tsx b/src/components/Tag/index.tsx index 6d19f6f..edc4047 100644 --- a/src/components/Tag/index.tsx +++ b/src/components/Tag/index.tsx @@ -4,7 +4,7 @@ import { useShallow } from "zustand/react/shallow"; import { useExplorer } from "src/hooks/useExplorer"; import { ExplorerStore } from "src/store"; import { TagNode } from "src/store/tag"; -import { pluralize } from "src/utils"; +import { isZh, pluralize } from "src/utils"; import TagContent from "./Content"; import TagExpandIcon from "./ExpandIcon"; @@ -19,66 +19,32 @@ const Tag = ({ disableHoverIndent = false, hideExpandIcon = false, }: Props) => { - const { useExplorerStore, plugin } = useExplorer(); - const { language } = plugin; + const { useExplorerStore } = useExplorer(); - const { - hasSubTag, - changeFocusedTag, - focusedTag, - getFilesInTag, - getTagsByParent, - expandedTagPaths, - collapseTag, - expandTag, - changeViewMode, - } = useExplorerStore( - useShallow((store: ExplorerStore) => ({ - hasSubTag: store.hasSubTag, - changeFocusedTag: store.changeFocusedTag, - focusedTag: store.focusedTag, - getTagsByParent: store.getTagsByParent, - getFilesInTag: store.getFilesInTag, - expandedTagPaths: store.expandedTagPaths, - collapseTag: store.collapseTag, - expandTag: store.expandTag, - changeViewMode: store.changeViewMode, - })) - ); + const { hasSubTag, focusedTag, getFilesInTag, getTagsByParent } = + useExplorerStore( + useShallow((store: ExplorerStore) => ({ + hasSubTag: store.hasSubTag, + focusedTag: store.focusedTag, + getTagsByParent: store.getTagsByParent, + getFilesInTag: store.getFilesInTag, + // for dependency tracking only + expandedTagPaths: store.expandedTagPaths, + })) + ); const isFocused = tag.fullPath == focusedTag?.fullPath; - const onToggleExpandState = (): void => { - const isFolderExpanded = expandedTagPaths.includes(tag.fullPath); - if (isFolderExpanded) { - collapseTag(tag); - } else { - expandTag(tag); - } - }; - - const onToggleSelectState = (): void => { - if (isFocused) { - changeFocusedTag(null); - changeViewMode("all"); - } else { - changeFocusedTag(tag); - changeViewMode("tag"); - } - }; - const getAriaLabel = () => { const { name } = tag; const filesCount = getFilesInTag(tag).length; const tagsCount = getTagsByParent(tag.fullPath).length; - if (language === "en") { - return `${name}\n${pluralize(filesCount, "file")}, ${pluralize( - tagsCount, - "tag" - )}`; - } - return `${name}\n${filesCount} 条笔记,${tagsCount} 个标签`; + if (isZh) return `${name}\n${filesCount} 条笔记,${tagsCount} 个标签`; + return `${name}\n${pluralize(filesCount, "file")}, ${pluralize( + tagsCount, + "tag" + )}`; }; const getIndentStyle = () => { @@ -104,10 +70,6 @@ const Tag = ({
{ - onToggleExpandState(); - onToggleSelectState(); - }} data-tooltip-position="right" aria-label={getAriaLabel()} > diff --git a/src/components/TogglableContainer.tsx b/src/components/TogglableContainer.tsx new file mode 100644 index 0000000..c9a1621 --- /dev/null +++ b/src/components/TogglableContainer.tsx @@ -0,0 +1,62 @@ +import { HTMLAttributes, MouseEvent, ReactNode, RefObject } from "react"; + +import { useExplorer } from "src/hooks/useExplorer"; +import { useExpandNodeByClick } from "src/hooks/useSettingsHandler"; +import { EXPAND_NODE_ON_CLICK } from "src/settings"; +import { AsyncNoop, Noop } from "src/utils"; + +import { NameRef } from "./EditableName"; +import ScrollInToViewContainer from "./ScrollInToViewContainer"; + +type Props = { + isFocused: boolean; + onFocus: AsyncNoop; + onToggle: Noop; + nameRef: RefObject; + children: ReactNode; +} & HTMLAttributes; +const TogglableContainer = ({ + nameRef, + isFocused, + onFocus, + onToggle, + children, + ...props +}: Props) => { + const { plugin } = useExplorer(); + + const { expandNodeByClick } = useExpandNodeByClick( + plugin.settings.expandNodeOnClick + ); + + const onClickContent = async (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + + if (!isFocused) { + await onFocus(); + } + if (nameRef.current && !nameRef.current.isFocusing) { + nameRef.current.setIsFocusing(true); + } + if (expandNodeByClick === EXPAND_NODE_ON_CLICK.LABEL) { + onToggle(); + } else if (expandNodeByClick === EXPAND_NODE_ON_CLICK.SELECTED_LABEL) { + if (nameRef.current?.isFocusing) { + onToggle(); + } + } + }; + + return ( + + {children} + + ); +}; + +export default TogglableContainer;