feat: add create new folder option in folder navigator modal

This commit is contained in:
wz 2025-10-07 21:15:30 +02:00
parent 95e5d2937f
commit d6f18b52c2
4 changed files with 236 additions and 13 deletions

View file

@ -5,6 +5,7 @@ import {
WorkspaceLeaf,
View,
FuzzyMatch,
Notice,
} from "obsidian";
import type FolderNavigatorPlugin from "./main";
import { FolderDisplayMode } from "./settings";
@ -53,6 +54,11 @@ interface FolderWithScore {
match: FuzzyMatch<TFolder>;
}
interface CreateFolderOption extends TFolder {
_isCreateOption: boolean;
_folderName: string;
}
export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
folders: TFolder[];
plugin: FolderNavigatorPlugin;
@ -439,6 +445,54 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
this.plugin,
);
// If query is provided, add "Create new folder" option at the end
if (normalizedQuery.length > 0) {
// Filter out separators to count real folder matches
const realFolderMatches = limitedResults.filter(
(result) => !(result.item as any)._isSeparator,
);
// Only show create option if the exact folder doesn't already exist
const exactMatch = realFolderMatches.some(
(result) =>
result.item.path.toLowerCase() === normalizedQuery ||
result.item.name.toLowerCase() === normalizedQuery,
);
if (!exactMatch) {
const createOption = {} as CreateFolderOption;
createOption._isCreateOption = true;
createOption._folderName = query.trim();
// Add separator before create option if there are results
if (limitedResults.length > 0) {
const separator = {} as TFolder;
(separator as any)._isSeparator = true;
(separator as any)._separatorText = "—————";
return [
...limitedResults,
{
item: separator,
match: { score: 0, matches: [] },
},
{
item: createOption as TFolder,
match: { score: 0, matches: [] },
},
];
} else {
// No results, just show create option
return [
{
item: createOption as TFolder,
match: { score: 0, matches: [] },
},
];
}
}
}
return limitedResults;
}
@ -451,6 +505,10 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
if ((folder as any)._isSeparator) {
return (folder as any)._separatorText;
}
// For create folder option
if ((folder as CreateFolderOption)._isCreateOption) {
return `Create new folder: ${(folder as CreateFolderOption)._folderName}`;
}
return folder.path;
}
@ -516,26 +574,55 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
}
/**
* Event handler for when a folder is chosen from the suggestion list
* Handle creating a new folder
*/
onChooseItem(folder: TFolder, evt: MouseEvent | KeyboardEvent): void {
// Check if this is a separator and ignore if so
if ((folder as any)._isSeparator) {
return;
private async handleCreateFolder(folderName: string): Promise<void> {
try {
log(`Creating new folder: "${folderName}"`, this.plugin);
// Get the base path from settings
const basePath = this.plugin.settings.newFolderLocation;
const fullPath = basePath
? `${basePath}/${folderName}`
: folderName;
log(`Full path for new folder: "${fullPath}"`, this.plugin);
// Check if folder already exists
const existingFolder =
this.app.vault.getAbstractFileByPath(fullPath);
if (existingFolder) {
new Notice(`Folder "${fullPath}" already exists`);
return;
}
// Create the folder
const newFolder = await this.app.vault.createFolder(fullPath);
log(`Folder created successfully: "${fullPath}"`, this.plugin);
new Notice(`Folder "${fullPath}" created successfully`);
// Close the modal
this.close();
// Navigate to the newly created folder
setTimeout(() => {
this.navigateToFolder(newFolder);
}, 100);
} catch (error) {
logError(`Error creating folder: ${error}`);
new Notice(`Failed to create folder: ${error.message || error}`);
}
}
log(
`Folder selected: "${folder.path}" (name: "${folder.name}")`,
this.plugin,
);
/**
* Navigate to a folder (same logic as in onChooseItem)
*/
private navigateToFolder(folder: TFolder): void {
try {
// Update folder history
this.updateFolderHistory(folder);
// Close the modal first
this.close();
// Make sure the file explorer is visible
const fileExplorerLeaves =
this.app.workspace.getLeavesOfType("file-explorer");
@ -609,6 +696,35 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
}
}
/**
* Event handler for when a folder is chosen from the suggestion list
*/
onChooseItem(folder: TFolder, evt: MouseEvent | KeyboardEvent): void {
// Check if this is a separator and ignore if so
if ((folder as any)._isSeparator) {
return;
}
// Check if this is a create folder option
if ((folder as CreateFolderOption)._isCreateOption) {
this.handleCreateFolder((folder as CreateFolderOption)._folderName);
return;
}
log(
`Folder selected: "${folder.path}" (name: "${folder.name}")`,
this.plugin,
);
// Close the modal first
this.close();
// Navigate to the selected folder
setTimeout(() => {
this.navigateToFolder(folder);
}, 100);
}
/**
* Expand a folder by finding and clicking its title directly in the DOM
*/
@ -747,6 +863,25 @@ export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
return;
}
// Check if this is a create folder option
if ((item.item as CreateFolderOption)._isCreateOption) {
el.empty();
const createEl = el.createDiv({
cls: "folder-create-option",
});
const iconEl = createEl.createSpan({
cls: "folder-create-icon",
});
iconEl.setText(" ");
const textEl = createEl.createSpan({
cls: "folder-create-text",
});
textEl.setText(
`Create new folder: "${(item.item as CreateFolderOption)._folderName}"`,
);
return;
}
// Custom rendering with keyword highlighting
el.empty();
const folder = item.item;

View file

@ -20,6 +20,7 @@ export interface PluginSettings {
accessCount: number; // visit count
}
>;
newFolderLocation: string; // Path where new folders should be created (empty string = root)
}
export const DEFAULT_SETTINGS: PluginSettings = {
@ -30,4 +31,5 @@ export const DEFAULT_SETTINGS: PluginSettings = {
recentFoldersToShow: 5,
frequentFoldersToShow: 5,
folderHistory: {},
newFolderLocation: "", // Empty string means root folder
};

View file

@ -5,10 +5,43 @@ import {
DropdownComponent,
Modal,
Notice,
FuzzySuggestModal,
TFolder,
} from "obsidian";
import FolderNavigatorPlugin from "./main";
import { FolderDisplayMode } from "./settings";
/**
* Modal for searching and selecting a folder for new folder creation location
*/
class FolderSelectionModal extends FuzzySuggestModal<TFolder> {
plugin: FolderNavigatorPlugin;
onSelect: (folder: TFolder) => void;
constructor(
app: App,
plugin: FolderNavigatorPlugin,
onSelect: (folder: TFolder) => void,
) {
super(app);
this.plugin = plugin;
this.onSelect = onSelect;
this.setPlaceholder("Search for a folder...");
}
getItems(): TFolder[] {
return this.app.vault.getAllFolders();
}
getItemText(folder: TFolder): string {
return folder.path || "/";
}
onChooseItem(folder: TFolder): void {
this.onSelect(folder);
}
}
export class SettingsTab extends PluginSettingTab {
plugin: FolderNavigatorPlugin;
@ -89,6 +122,43 @@ export class SettingsTab extends PluginSettingTab {
}),
);
// New folder creation section
new Setting(containerEl).setName("Folder creation").setHeading();
new Setting(containerEl)
.setName("Default location for new folders")
.setDesc(
"Choose where new folders will be created when using the 'Create folder' option. Default is the vault root.",
)
.addButton((button) => {
const displayPath =
this.plugin.settings.newFolderLocation || "/ (Root)";
button.setButtonText(displayPath).onClick(() => {
const modal = new FolderSelectionModal(
this.app,
this.plugin,
async (folder: TFolder) => {
this.plugin.settings.newFolderLocation =
folder.path;
await this.plugin.saveSettings();
button.setButtonText(folder.path || "/ (Root)");
},
);
modal.open();
});
})
.addExtraButton((button) => {
button
.setIcon("reset")
.setTooltip("Reset to root folder")
.onClick(async () => {
this.plugin.settings.newFolderLocation = "";
await this.plugin.saveSettings();
// Update button text by recreating the setting
this.display();
});
});
// Folder display preferences section
new Setting(containerEl)
.setName("Folder display preferences")

View file

@ -18,3 +18,19 @@
.suggestion-highlight {
font-weight: bold;
}
.folder-create-option {
display: flex;
align-items: center;
padding: 4px 0;
color: var(--text-accent);
}
.folder-create-icon {
margin-right: 6px;
font-size: 1.1em;
}
.folder-create-text {
font-style: italic;
}