mirror of
https://github.com/xuquan-nikkkki/FolderFile-Splitter-Plugin.git
synced 2026-07-22 05:37:28 +00:00
refactor: put duplicate logic of folder/tag/file name component into EditableName
This commit is contained in:
parent
2ecf5a1feb
commit
5ee2d88db2
12 changed files with 472 additions and 321 deletions
168
src/components/EditableName/index.tsx
Normal file
168
src/components/EditableName/index.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
32
src/components/ScrollInToViewContainer.tsx
Normal file
32
src/components/ScrollInToViewContainer.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
39
src/components/Tag/Name.tsx
Normal file
39
src/components/Tag/Name.tsx
Normal 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;
|
||||
|
|
@ -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()}
|
||||
>
|
||||
|
|
|
|||
62
src/components/TogglableContainer.tsx
Normal file
62
src/components/TogglableContainer.tsx
Normal 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;
|
||||
Loading…
Reference in a new issue