feat: add click to toggle folder option in settings

This commit is contained in:
Xu Quan 2025-07-11 16:42:43 +08:00
parent e7b47c27ea
commit 2ecf5a1feb
16 changed files with 222 additions and 86 deletions

View file

@ -125,6 +125,7 @@ export class SettingTab extends PluginSettingTab {
this.createHeader2(this.headersCopy.folderAndTagBehavior);
this._initToggleSetting("showFolderHierarchyLines");
this._initToggleSetting("showFilesCount");
this._initDropdownSetting("expandNodeOnClick")
this._initToggleSetting("openDestinationFolderAfterMove");
this._initToggleSetting("revealFileInExplorer");
}

View file

@ -5,13 +5,15 @@ import { ChevronRight } from "src/assets/icons";
type Props = {
isExpanded: boolean;
hideIcon: boolean;
onToggle: () => void;
};
const ExpandIcon = ({ isExpanded, hideIcon }: Props) => {
const ExpandIcon = ({ isExpanded, hideIcon, onToggle }: Props) => {
return (
<div
className={classNames("tree-item-icon collapse-icon", {
"is-collapsed": !isExpanded,
})}
onClick={onToggle}
>
{hideIcon ? null : (
<ChevronRight className="ffs__expand-icon svg-icon right-triangle" />

View file

@ -1,10 +1,12 @@
import classNames from "classnames";
import { Menu, TFolder } from "obsidian";
import { useEffect, useRef } from "react";
import { MouseEvent, 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,
@ -22,7 +24,8 @@ import { FolderListModal } from "../FolderListModal";
import FolderContentInner from "./ContentInner";
export type FolderInnerContentRef = {
export type FolderNameRef = {
isFocusing: boolean;
setIsFocusing: (isFocusing: boolean) => void;
onStartEditingName: Noop;
};
@ -33,40 +36,42 @@ export type FolderProps = {
type Props = FolderProps;
const FolderContent = ({ folder }: Props) => {
const { useExplorerStore, plugin } = useExplorer();
const { language } = plugin;
const { language, settings } = plugin;
const {
focusedFolder,
changeFocusedFolder,
createNewFolder,
createFileWithDefaultName,
createNewFolderAndFocus,
createNewFileAndFocus,
pinFolder,
unpinFolder,
isFolderPinned,
trashFolder,
expandFolder,
isFolderExpanded
toggleFolder,
} = useExplorerStore(
useShallow((store: ExplorerStore) => ({
focusedFolder: store.focusedFolder,
changeFocusedFolder: store.changeFocusedFolder,
createNewFolder: store.createNewFolder,
createFileWithDefaultName: store.createFileWithDefaultName,
createNewFolderAndFocus: store.createNewFolderAndFocus,
createNewFileAndFocus: store.createNewFileAndFocus,
pinFolder: store.pinFolder,
unpinFolder: store.unpinFolder,
isFolderPinned: store.isFolderPinned,
trashFolder: store.trashFolder,
expandFolder: store.expandFolder,
isFolderExpanded: store.isFolderExpanded
toggleFolder: store.toggleFolder,
}))
);
const isFocused = folder.path == focusedFolder?.path;
const { expandNodeByClick } = useExpandNodeByClick(
settings.expandNodeOnClick
);
const folderRef = useRef<HTMLDivElement>(null);
const innerContentRef = useRef<FolderInnerContentRef>(null);
const nameRef = useRef<FolderNameRef>(null);
useEffect(() => {
nameRef.current?.setIsFocusing(false)
if (focusedFolder?.path !== folder.path) return;
setTimeout(() => {
folderRef.current?.scrollIntoView({
@ -90,28 +95,11 @@ const FolderContent = ({ folder }: Props) => {
modal.open();
};
const onCreateNewFile = async () => {
if (folder.path !== focusedFolder?.path) {
await changeFocusedFolder(folder);
}
await createFileWithDefaultName(folder);
};
const onCreateNewFolder = async () => {
const newFolder = await createNewFolder(folder);
if (!isFolderExpanded(folder)) {
expandFolder(folder);
}
if (newFolder) {
await changeFocusedFolder(newFolder);
}
};
const addCreateFolderMenuItem = (menu: Menu) => {
addMenuItem(menu, {
title: FOLDER_OPERATION_COPY.createFolder[language],
icon: "folder-open",
action: onCreateNewFolder,
action: async () => await createNewFolderAndFocus(folder),
});
};
@ -125,7 +113,10 @@ const FolderContent = ({ folder }: Props) => {
addPinInMenu(menu);
menu.addSeparator();
addCreateFileMenuItem(menu, onCreateNewFile);
addCreateFileMenuItem(
menu,
async () => await createNewFileAndFocus(folder)
);
addCreateFolderMenuItem(menu);
menu.addSeparator();
@ -134,7 +125,7 @@ const FolderContent = ({ folder }: Props) => {
addRenameMenuItem(
menu,
innerContentRef?.current?.onStartEditingName ?? noop,
nameRef?.current?.onStartEditingName ?? noop,
isRootFolder
);
addDeleteMenuItem(menu, () => trashFolder(folder));
@ -142,18 +133,29 @@ 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")}
onContextMenu={onShowContextMenu}
onClick={() => {
if (isFocused) {
folderRef.current?.focus();
innerContentRef.current?.setIsFocusing(true);
}
}}
onClick={onClickFolderContent}
>
<FolderContentInner folder={folder} />
<FolderContentInner folder={folder} ref={folderRef} nameRef={nameRef} />
</div>
);
};

View file

@ -1,23 +1,23 @@
import { TFolder } from "obsidian";
import { ForwardedRef, forwardRef, useRef } from "react";
import { ForwardedRef, forwardRef, Ref } from "react";
import { FolderInnerContentRef } from "./Content";
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 }: Props, ref: ForwardedRef<FolderInnerContentRef>) => {
const folderRef = useRef<HTMLDivElement>(null);
({ folder, nameRef }: Props, ref: ForwardedRef<HTMLDivElement | null>) => {
return (
<div ref={folderRef} className="ffs__folder-content--main">
<div ref={ref} className="ffs__folder-content--main">
<FolderIcon folder={folder} />
<FolderName folder={folder} ref={ref} />
<FolderName folder={folder} ref={nameRef} />
<FolderFilesCount folder={folder} />
</div>
);

View file

@ -12,10 +12,11 @@ type Props = {
const FolderExpandIcon = ({ folder }: Props) => {
const { useExplorerStore } = useExplorer();
const { hasSubFolder, isFolderExpanded } = useExplorerStore(
const { hasSubFolder, isFolderExpanded, toggleFolder } = useExplorerStore(
useShallow((store: ExplorerStore) => ({
hasSubFolder: store.hasSubFolder,
isFolderExpanded: store.isFolderExpanded,
toggleFolder: store.toggleFolder,
}))
);
@ -23,6 +24,7 @@ const FolderExpandIcon = ({ folder }: Props) => {
<ExpandIcon
isExpanded={isFolderExpanded(folder)}
hideIcon={!hasSubFolder(folder)}
onToggle={() => toggleFolder(folder)}
/>
);
};

View file

@ -13,43 +13,39 @@ import { useExplorer } from "src/hooks/useExplorer";
import useRenderEditableName from "src/hooks/useRenderEditableName";
import { ExplorerStore } from "src/store";
import { FolderInnerContentRef } from "./Content";
import { FolderNameRef } from "./Content";
export type FolderProps = {
folder: TFolder;
};
type Props = FolderProps;
const FolderName = forwardRef(
({ folder }: Props, ref: ForwardedRef<FolderInnerContentRef>) => {
({ folder }: Props, ref: ForwardedRef<FolderNameRef>) => {
const { useExplorerStore } = useExplorer();
const {
renameFolder,
getNameOfFolder,
isLastCreatedFolder,
} = useExplorerStore(
useShallow((store: ExplorerStore) => ({
renameFolder: store.renameFolder,
getNameOfFolder: store.getNameOfFolder,
isLastCreatedFolder: store.isLastCreatedFolder,
}))
);
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 { 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
onStartEditingName,
isFocusing,
}));
const onClickOutside = (event: MouseEvent) => {
@ -63,7 +59,7 @@ const FolderName = forwardRef(
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter" && isFocusing) {
onStartEditingName()
onStartEditingName();
}
};

View file

@ -27,9 +27,7 @@ const Folder = ({
const { useExplorerStore } = useExplorer();
const {
changeFocusedFolder,
expandFolder,
toggleFolder,
focusedFolder,
hasSubFolder,
getFilesinFolder,
@ -37,10 +35,8 @@ const Folder = ({
getFolderLevel,
} = useExplorerStore(
useShallow((store: ExplorerStore) => ({
changeFocusedFolder: store.changeFocusedFolder,
hasSubFolder: store.hasSubFolder,
expandFolder: store.expandFolder,
toggleFolder: store.toggleFolder,
focusedFolder: store.focusedFolder,
getFilesinFolder: store.getFilesInFolder,
getSubFolders: store.getSubFolders,
@ -97,10 +93,6 @@ const Folder = ({
<div
className={getClassNames()}
ref={setDropRef}
onClick={async () => {
toggleFolder(folder);
await changeFocusedFolder(folder);
}}
style={getIndentStyle(getFolderLevel(folder), disableHoverIndent)}
data-tooltip-position="right"
aria-label={getAriaLabel()}

View file

@ -40,9 +40,9 @@ const FolderAndTagTree = () => {
if (!showFolderView) return;
const sortedFolders = sortFolders(folders);
return sortedFolders.map((folder) => {
return <FolderTreeItem folder={folder} key={folder.path} />;
});
return sortedFolders.map((folder) => (
<FolderTreeItem folder={folder} key={folder.path} />
));
};
const renderTags = (tags: TagNode[]) => {

View file

@ -12,10 +12,11 @@ type Props = {
const TagExpandIcon = ({ tag }: Props) => {
const { useExplorerStore } = useExplorer();
const { hasSubTag, isTagExpanded } = useExplorerStore(
const { hasSubTag, isTagExpanded, toggleTag } = useExplorerStore(
useShallow((store: ExplorerStore) => ({
hasSubTag: store.hasSubTag,
isTagExpanded: store.isTagExpanded,
toggleTag: store.toggleTag,
}))
);
@ -23,6 +24,7 @@ const TagExpandIcon = ({ tag }: Props) => {
<ExpandIcon
isExpanded={isTagExpanded(tag)}
hideIcon={!hasSubTag(tag)}
onToggle={() => toggleTag(tag)}
/>
);
};

View file

@ -1,5 +1,6 @@
import { useAutoHideActionBar } from "./useAutoHideActionBar";
import { useBoldFileTitle } from "./useBoldFileTitle";
import { useExpandNodeByClick } from "./useExpandNodeByClick";
import { useFileCreationDateFormat } from "./useFileCreationDateFormat";
import { useFileItemSpacing } from "./useFileItemSpacing";
import { useHideRootFolder } from "./useHideRootFolder";
@ -42,4 +43,5 @@ export {
useIncludeSubTagFiles,
useShowTagIcon,
useShowFilesCount,
useExpandNodeByClick
};

View file

@ -0,0 +1,23 @@
import { useState } from "react";
import { ExpandNodeOnClick } from "src/settings";
import { useWatchSettingsChange } from "./useWatchSettingsChange";
export const useExpandNodeByClick = (
defaultExpandNodeByClick: ExpandNodeOnClick
): { expandNodeByClick: ExpandNodeOnClick } => {
const [expandNodeByClick, setExpandNodeByClick] =
useState<ExpandNodeOnClick>(defaultExpandNodeByClick);
const onChangeExpand = (event: CustomEvent) => {
const { changeKey, changeValue } = event.detail;
if (changeKey == "expandNodeByClick") {
setExpandNodeByClick(changeValue);
}
};
useWatchSettingsChange(onChangeExpand);
return { expandNodeByClick };
};

View file

@ -1,4 +1,5 @@
import {
EXPAND_NODE_ON_CLICK,
FILE_ITEM_SPACING,
FileDetailSettings,
FileDisplaySettings,
@ -145,6 +146,44 @@ export const FOLDER_AND_TAG_BEHAVIOR_SETTINGS_COPY: SettingsLocaleResource<Folde
desc: "在每个文件夹或标签名称右侧显示包含的文件数量。",
},
},
expandNodeOnClick: {
en: {
name: "Expand node on click",
desc: "Control how folders or tags expand when clicked: only when clicking the icon, or also when clicking the label (with optional selection behavior).",
options: [
{
value: EXPAND_NODE_ON_CLICK.ICON,
text: "Toggle icon only",
},
{
value: EXPAND_NODE_ON_CLICK.LABEL,
text: "Click label to expand and select",
},
{
value: EXPAND_NODE_ON_CLICK.SELECTED_LABEL,
text: "Click once to select, click again to expand",
},
],
},
zh: {
name: "点击展开节点",
desc: "设置点击文件夹或标签时的展开方式:仅点击图标展开,点击标签名时直接展开并选中,或点击一次选中、再次点击展开。",
options: [
{
value: EXPAND_NODE_ON_CLICK.ICON,
text: "仅点击图标",
},
{
value: EXPAND_NODE_ON_CLICK.LABEL,
text: "点击文件夹/标签名展开并选中",
},
{
value: EXPAND_NODE_ON_CLICK.SELECTED_LABEL,
text: "第一次点击选中,再次点击展开",
},
],
},
},
openDestinationFolderAfterMove: {
en: {
name: "Open destination folder after move",

View file

@ -15,7 +15,14 @@ import {
} from "./assets/constants";
import { ExplorerView } from "./ExplorerView";
import { Lang } from "./locales";
import { DEFAULT_SETTINGS, FolderFileSplitterPluginSettings } from "./settings";
import {
AllSettings,
DEFAULT_SETTINGS,
ExpandFolderByClickingOnElement,
ExpandNodeOnClick,
FolderFileSplitterPluginSettings,
LegacySettings,
} from "./settings";
import { SettingTab } from "./SettingTab";
export default class FolderFileSplitterPlugin extends Plugin {
@ -159,7 +166,7 @@ export default class FolderFileSplitterPlugin extends Plugin {
};
onunload() {
console.log("Unloading FolderFile Splitter...")
console.log("Unloading FolderFile Splitter...");
this.detachFileTreeLeafs();
this.app.vault.off("create", this.onCreate);
this.app.vault.off("modify", this.onModify);
@ -183,11 +190,37 @@ export default class FolderFileSplitterPlugin extends Plugin {
}
};
migrateLegacySettings(raw: Partial<AllSettings>): Partial<AllSettings> {
const updated = { ...raw };
const legacyExpandNodeOnClick = raw.expandFolderByClickingOn;
if (legacyExpandNodeOnClick && !raw.expandNodeOnClick) {
const valueMap: Record<
ExpandFolderByClickingOnElement,
ExpandNodeOnClick
> = {
icon: "icon",
folder: "selected_label",
};
updated.expandNodeOnClick = valueMap[legacyExpandNodeOnClick];
}
return updated;
}
removeLegacySettings(
raw: Partial<AllSettings>
): FolderFileSplitterPluginSettings {
return Object.fromEntries(
Object.entries(raw).filter(([key]) => key in DEFAULT_SETTINGS)
) as FolderFileSplitterPluginSettings;
}
async loadSettings() {
const rawData = (await this.loadData()) ?? {};
const filteredData = Object.fromEntries(
Object.entries(rawData).filter(([key]) => key in DEFAULT_SETTINGS)
);
const migratedData = this.migrateLegacySettings(rawData);
const filteredData = this.removeLegacySettings(migratedData);
this.settings = Object.assign({}, DEFAULT_SETTINGS, filteredData);
}

View file

@ -1,5 +1,12 @@
export type ValueOf<T> = T[keyof T];
export const EXPAND_NODE_ON_CLICK = {
ICON: "icon",
LABEL: "label",
SELECTED_LABEL: "selected_label",
} as const;
export type ExpandNodeOnClick = ValueOf<typeof EXPAND_NODE_ON_CLICK>;
export const LAYOUT_MODE = {
HORIZONTAL_SPLIT: "Horizontal split",
VERTICAL_SPLIT: "Vertical split",
@ -42,6 +49,7 @@ export type LayoutSettings = {
export type FolderAndTagBehaviorSettings = {
showFolderHierarchyLines: boolean;
showFilesCount: boolean;
expandNodeOnClick: ExpandNodeOnClick;
openDestinationFolderAfterMove: boolean;
revealFileInExplorer: boolean;
};
@ -96,6 +104,7 @@ export const LAYOUT_SETTINGS: LayoutSettings = {
export const FOLDER_AND_TAG_BEHAVIOR_SETTINGS: FolderAndTagBehaviorSettings = {
showFolderHierarchyLines: false,
showFilesCount: true,
expandNodeOnClick: EXPAND_NODE_ON_CLICK.SELECTED_LABEL,
openDestinationFolderAfterMove: false,
revealFileInExplorer: false,
};
@ -144,3 +153,21 @@ export const DEFAULT_SETTINGS: FolderFileSplitterPluginSettings = {
...FILE_DISPLAY_SETTINGS,
...FOLDER_NOTE_SETTINGS,
};
// legacy constant and type
const EXPAND_FOLDER_BY_CLICKING_ELEMENT = {
ICON: "icon",
FOLDER: "folder",
} as const;
export type ExpandFolderByClickingOnElement = ValueOf<
typeof EXPAND_FOLDER_BY_CLICKING_ELEMENT
>;
export type LegacySettings = {
expandFolderByClickingOn: ExpandFolderByClickingOnElement;
};
export const LEGACY_SETTINGS = {
expandFolderByClickingOn: EXPAND_FOLDER_BY_CLICKING_ELEMENT.FOLDER,
};
export type AllSettings = FolderFileSplitterPluginSettings & LegacySettings;

View file

@ -16,6 +16,7 @@ export interface FolderActionsSlice {
createNewFolder: (parentFolder: TFolder) => Promise<TFolder | undefined>;
createNewFolderAndFocus: (parentFolder: TFolder) => Promise<void>;
createNewFileAndFocus: (parentFolder: TFolder) => Promise<void>;
moveFolder: (folder: TFolder, newPath: string) => Promise<void>;
renameFolder: (folder: TFolder, newName: string) => Promise<void>;
@ -76,6 +77,11 @@ export const createFolderActionsSlice =
await changeFocusedFolder(newFolder);
}
},
createNewFileAndFocus: async (parentFolder: TFolder) => {
const { createFileWithDefaultName, changeFocusedFolder } = get();
await createFileWithDefaultName(parentFolder);
await changeFocusedFolder(parentFolder);
},
trashFolder: async (folder: TFolder) => {
const { focusedFolder, isAnscestorOf, setFocusedFolderAndSave } =
get();

View file

@ -19,6 +19,7 @@ export interface ToggleTagSlice {
expandTag: (tag: TagNode) => void;
expandAllTags: () => void;
collapseTag: (tag: TagNode) => void;
toggleTag: (tag: TagNode) => void;
collapseAllTags: () => void;
restoreExpandedTagPaths: () => void;
@ -72,6 +73,14 @@ export const createToggleTagSlice =
removeItemFromArray(expandedTagPaths, tag.fullPath)
);
},
toggleTag: (tag: TagNode) => {
const { isTagExpanded, expandTag, collapseTag } = get();
if (isTagExpanded(tag)) {
collapseTag(tag);
} else {
expandTag(tag);
}
},
collapseAllTags: () => {
get().changeExpandedTagPaths([]);
},