refactor: put duplicate logic of folder/tag/file name component into EditableName

This commit is contained in:
Xu Quan 2025-07-11 23:02:42 +08:00
parent 2ecf5a1feb
commit 5ee2d88db2
12 changed files with 472 additions and 321 deletions

View file

@ -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<void>;
contentRef: RefObject<HTMLDivElement | null>;
isFocused: boolean;
className?: string;
boldName?: boolean;
};
const EditableName = forwardRef(
(
{
defaultName,
onSaveName,
isFocused,
className = "",
boldName = false,
contentRef,
}: Props,
ref
) => {
const eleRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [isEditing, setIsEditing] = useState(false);
const [isFocusing, setIsFocusing] = useState<boolean>(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<HTMLDivElement>
): 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 = () => (
<input
ref={inputRef}
className="ffs__name-input"
value={name}
onKeyDown={onKeyDownInInput}
onChange={(e) => setName(e.target.value)}
/>
);
const renderNameDisplay = () => (
<div
ref={eleRef}
className={classNames("ffs__name", className, {
"ffs__name--bold": boldName,
})}
>
{name}
</div>
);
return isEditing ? renderInput() : renderNameDisplay();
}
);
export default EditableName;

View file

@ -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<HTMLDivElement>(null);
const innerContentRef = useRef<FileInnerContentRef>(null);
const nameRef = useRef<NameRef>(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 (
<div
<ScrollInToViewContainer
needToScroll={isFocused}
className={getClassNames()}
ref={fileRef}
onContextMenu={onShowContextMenu}
onClick={onClickFile}
>
<FileContentInner
file={file}
ref={innerContentRef}
fileRef={fileRef}
/>
</div>
<FileContentInner file={file} ref={nameRef} />
</ScrollInToViewContainer>
);
};

View file

@ -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<HTMLDivElement | null>;
};
const FileContentInner = forwardRef(
(
{ file, fileRef }: Props,
ref: React.ForwardedRef<FileInnerContentRef>
) => {
({ file }: Props, ref: RefObject<NameRef>) => {
const { plugin } = useExplorer();
const { settings } = plugin;
@ -30,6 +28,8 @@ const FileContentInner = forwardRef(
const { showFileDetail } = useShowFileDetail(showDetail);
const { fileItemSpacing } = useFileItemSpacing(spacing);
const contentRef = useRef<HTMLDivElement>(null)
const maybeRenderFileDetail = () => {
if (!showFileDetail) return null;
return <FileDetail file={file} />;
@ -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 (
<div className={getClassNames()}>
<div className={getClassNames()} ref={contentRef}>
<div className="ffs__file-content-title">
<FileName file={file} ref={ref} fileRef={fileRef} />
<FileName file={file} ref={ref} contentRef={contentRef} />
<FileExtension file={file} />
</div>
{maybeRenderFileDetail()}

View file

@ -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<HTMLDivElement | null>
}
const FileName = forwardRef(
(
{ file, fileRef }: Props,
ref: React.ForwardedRef<FileInnerContentRef>
) => {
({ file, contentRef }: Props, ref: RefObject<NameRef>) => {
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<boolean>(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 (
<EditableName
ref={ref}
contentRef={contentRef}
defaultName={file.basename}
onSaveName={async (name: string) =>
await renameFile(file, name)
}
className="ffs__file-name"
boldName={boldFileTitle}
isFocused={isFocusedFile(file)}
/>
);
}
);

View file

@ -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<NameRef>(null);
const contentRef = useRef<HTMLDivElement>(null);
const folderRef = useRef<HTMLDivElement>(null);
const nameRef = useRef<FolderNameRef>(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 (
<div
className={classNames("ffs__folder")}
<TogglableContainer
nameRef={nameRef}
isFocused={isFocused}
onFocus={async () => await changeFocusedFolder(folder)}
onToggle={() => toggleFolder(folder)}
className="ffs__folder"
onContextMenu={onShowContextMenu}
onClick={onClickFolderContent}
>
<FolderContentInner folder={folder} ref={folderRef} nameRef={nameRef} />
</div>
<div className="ffs__folder-content--main" ref={contentRef}>
<FolderIcon folder={folder} />
<FolderName
folder={folder}
ref={nameRef}
contentRef={contentRef}
/>
<FolderFilesCount folder={folder} />
</div>
</TogglableContainer>
);
};

View file

@ -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<FolderNameRef>
};
type Props = FolderProps;
const FolderContentInner = forwardRef(
({ folder, nameRef }: Props, ref: ForwardedRef<HTMLDivElement | null>) => {
return (
<div ref={ref} className="ffs__folder-content--main">
<FolderIcon folder={folder} />
<FolderName folder={folder} ref={nameRef} />
<FolderFilesCount folder={folder} />
</div>
);
}
);
export default FolderContentInner;

View file

@ -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<HTMLDivElement | null>;
};
type Props = FolderProps;
const FolderName = forwardRef(
({ folder }: Props, ref: ForwardedRef<FolderNameRef>) => {
({ folder, contentRef }: Props, ref: RefObject<NameRef>) => {
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<HTMLDivElement>(null);
const [isFocusing, setIsFocusing] = useState<boolean>(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 (
<EditableName
ref={ref}
isFocused={isFocusedFolder(folder)}
contentRef={contentRef}
defaultName={getNameOfFolder(folder)}
className="ffs__folder-name"
onSaveName={async (name: string) =>
await renameFolder(folder, name)
}
/>
);
}
);

View file

@ -0,0 +1,32 @@
import { HTMLAttributes, ReactNode, useEffect, useRef } from "react";
type Props = {
needToScroll: boolean;
children: ReactNode;
} & HTMLAttributes<HTMLDivElement>;
const ScrollInToViewContainer = ({
needToScroll,
children,
...props
}: Props) => {
const eleRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (needToScroll) {
setTimeout(() => {
eleRef.current?.scrollIntoView({
behavior: "smooth",
block: "center",
});
}, 100);
}
}, [needToScroll]);
return (
<div ref={eleRef} {...props}>
{children}
</div>
);
};
export default ScrollInToViewContainer;

View file

@ -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<NameRef>(null);
const contentRef = useRef<HTMLDivElement | null>(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 = () => (
<div className="ffs__tag-content--main" ref={tagRef}>
<div className="ffs__tag-content--main" ref={contentRef}>
<TagIcon />
{renderTagName()}
<TagName tag={tag} ref={nameRef} contentRef={contentRef} />
<TagFilesCount tag={tag} />
</div>
);
return (
<div
className={classNames("ffs__tag")}
<TogglableContainer
nameRef={nameRef}
isFocused={isFocused}
onFocus={async () => await changeFocusedTag(tag)}
onToggle={() => toggleTag(tag)}
className="ffs__tag"
onContextMenu={onShowContextMenu}
onClick={(e) => {
if (isFocused) {
tagRef.current?.focus();
setIsFocusing(true);
}
}}
>
{renderTitleContent()}
</div>
</TogglableContainer>
);
};

View file

@ -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<HTMLDivElement | null>;
};
type Props = FolderProps;
const TagName = forwardRef(
({ tag, contentRef }: Props, ref: RefObject<NameRef>) => {
const { useExplorerStore } = useExplorer();
const { renameTag, isFocusedTag } = useExplorerStore(
useShallow((store: ExplorerStore) => ({
renameTag: store.renameTag,
isFocusedTag: store.isFocusedTag,
}))
);
return (
<EditableName
ref={ref}
isFocused={isFocusedTag(tag)}
contentRef={contentRef}
defaultName={tag.name}
className="ffs__tag-name"
onSaveName={async (name: string) => await renameTag(tag, name)}
/>
);
}
);
export default TagName;

View file

@ -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 = ({
<div
className={getClassNames()}
style={getIndentStyle()}
onClick={() => {
onToggleExpandState();
onToggleSelectState();
}}
data-tooltip-position="right"
aria-label={getAriaLabel()}
>

View file

@ -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<NameRef | null>;
children: ReactNode;
} & HTMLAttributes<HTMLDivElement>;
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 (
<ScrollInToViewContainer
needToScroll={isFocused}
onClick={onClickContent}
{...props}
>
{children}
</ScrollInToViewContainer>
);
};
export default TogglableContainer;