├── .prettierrc.cjs ├── .release-it.cjs ├── .yarnrc.yml ├── .release-it.lib.cjs ├── .release-it.ob.cjs ├── .eslintrc ├── tsconfig.json ├── src ├── logger.ts ├── typings │ ├── obsidian-ex.d.ts │ └── api.ts ├── index.ts ├── misc.ts ├── fnc-main.ts ├── modules │ ├── commands.ts │ ├── vault-handler.ts │ └── resolver.ts └── settings.ts ├── tsconfig-lib.json ├── manifest.json ├── .gitignore ├── scripts └── cp-dts.mjs ├── versions.json ├── esbuild.config.mjs ├── LICENSE ├── package.json ├── README.md └── CHANGELOG.md /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require("@aidenlx/prettier-config"); 2 | -------------------------------------------------------------------------------- /.release-it.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require("@aidenlx/release-it-config").full; 2 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: pnpm 2 | 3 | yarnPath: .yarn/releases/yarn-3.1.1.cjs 4 | -------------------------------------------------------------------------------- /.release-it.lib.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require("@aidenlx/release-it-config").libOnly; 2 | -------------------------------------------------------------------------------- /.release-it.ob.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require("@aidenlx/release-it-config").obOnly; 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@aidenlx/eslint-config", 3 | "ignorePatterns": ["lib/**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@aidenlx/ts-config/tsconfig.json", 3 | "include": ["src/**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import defaultLog from "loglevel"; 2 | const log = defaultLog.getLogger("folder-note-core"); 3 | export default log; 4 | -------------------------------------------------------------------------------- /tsconfig-lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@aidenlx/ts-config/tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "isolatedModules": true, 6 | "outDir": "lib", 7 | "declaration": true, 8 | "inlineSourceMap": true 9 | }, 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "folder-note-core", 3 | "name": "Folder Note Core", 4 | "version": "1.3.5", 5 | "minAppVersion": "0.13.24", 6 | "description": "Provide core features and API for folder notes", 7 | "author": "AidenLx", 8 | "authorUrl": "https://github.com/aidenlx", 9 | "isDesktopOnly": false 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | node_modules 3 | package-lock.json 4 | # yarn.lock 5 | 6 | # build 7 | build 8 | 9 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 10 | .pnp.* 11 | .yarn/* 12 | !.yarn/patches 13 | !.yarn/plugins 14 | !.yarn/releases 15 | !.yarn/sdks 16 | !.yarn/versions 17 | 18 | lib -------------------------------------------------------------------------------- /scripts/cp-dts.mjs: -------------------------------------------------------------------------------- 1 | import glob from "fast-glob"; 2 | import { promises as fs } from "fs"; 3 | import { dirname } from "path"; 4 | 5 | const copy = async (srcPath) => { 6 | const cpTo = dts.replace("src/", "lib/"); 7 | await fs.mkdir(dirname(cpTo), { recursive: true }); 8 | await fs.copyFile(dts, cpTo); 9 | }; 10 | 11 | await glob("src/**/*.d.ts").map(copy); 12 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.1.0": "0.12.5", 3 | "0.2.0": "0.12.5", 4 | "1.0.0": "0.12.5", 5 | "1.0.1": "0.12.5", 6 | "1.0.2": "0.12.5", 7 | "1.1.0": "0.12.5", 8 | "1.2.0": "0.12.5", 9 | "1.2.1": "0.12.5", 10 | "1.2.2": "0.12.5", 11 | "1.2.3": "0.12.5", 12 | "1.2.4": "0.12.5", 13 | "1.2.5": "0.12.5", 14 | "1.2.6": "0.12.5", 15 | "1.3.0": "0.12.5", 16 | "1.3.1": "0.12.5", 17 | "1.3.2": "0.12.5", 18 | "1.3.3": "0.12.5", 19 | "1.3.4": "0.13.24", 20 | "1.3.5": "0.13.24" 21 | } -------------------------------------------------------------------------------- /src/typings/obsidian-ex.d.ts: -------------------------------------------------------------------------------- 1 | import "obsidian"; 2 | 3 | import FolderNoteAPI from "./api"; 4 | 5 | declare module "obsidian" { 6 | interface Vault { 7 | exists(normalizedPath: string, sensitive?: boolean): Promise; 8 | getConfig(key: string): any; 9 | } 10 | 11 | interface App { 12 | plugins: { 13 | enabledPlugins: Set; 14 | plugins: { 15 | ["folder-note-core"]?: { 16 | api: FolderNoteAPI; 17 | }; 18 | }; 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import obPlugin from "@aidenlx/esbuild-plugin-obsidian"; 2 | import { build } from "esbuild"; 3 | 4 | const banner = `/* 5 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 6 | if you want to view the source visit the plugins github repository 7 | */ 8 | `; 9 | 10 | const isProd = process.env.BUILD === "production"; 11 | 12 | try { 13 | await build({ 14 | entryPoints: ["src/fnc-main.ts"], 15 | bundle: true, 16 | watch: !isProd, 17 | platform: "browser", 18 | external: ["obsidian"], 19 | format: "cjs", 20 | mainFields: ["browser", "module", "main"], 21 | banner: { js: banner }, 22 | sourcemap: isProd ? false : "inline", 23 | minify: isProd, 24 | define: { 25 | "process.env.NODE_ENV": JSON.stringify(process.env.BUILD), 26 | }, 27 | outfile: "build/main.js", 28 | plugins: [obPlugin()], 29 | }); 30 | } catch (err) { 31 | console.error(err); 32 | process.exit(1); 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021-present AidenLx 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "obsidian"; 2 | 3 | import { Plugin } from "obsidian"; 4 | 5 | import FolderNoteAPI, { FNCEvents, NoteLoc } from "./typings/api"; 6 | export type { FNCEvents, FolderNoteAPI, NoteLoc }; 7 | 8 | // EVENTS 9 | 10 | type OnArgs = T extends [infer A, ...infer B] 11 | ? A extends string 12 | ? [name: A, callback: (...args: B) => any] 13 | : never 14 | : never; 15 | type EventsOnArgs = OnArgs; 16 | 17 | declare module "obsidian" { 18 | interface Vault { 19 | on(...args: EventsOnArgs): EventRef; 20 | } 21 | } 22 | 23 | // UTIL FUNCTIONS 24 | 25 | export const getApi = (plugin?: Plugin): FolderNoteAPI | undefined => { 26 | if (plugin) return plugin.app.plugins.plugins["folder-note-core"]?.api; 27 | else return window["FolderNoteAPIv0"]; 28 | }; 29 | 30 | export const isPluginEnabled = (plugin: Plugin) => 31 | plugin.app.plugins.enabledPlugins.has("folder-note-core"); 32 | 33 | export const registerApi = ( 34 | plugin: Plugin, 35 | callback: (api: FolderNoteAPI) => void, 36 | ): FolderNoteAPI | undefined => { 37 | plugin.app.vault.on("folder-note:api-ready", callback); 38 | return getApi(plugin); 39 | }; 40 | -------------------------------------------------------------------------------- /src/misc.ts: -------------------------------------------------------------------------------- 1 | import { TAbstractFile, TFile, TFolder } from "obsidian"; 2 | import { dirname, join } from "path"; 3 | 4 | import log from "./logger"; 5 | 6 | export const isMd = (file: TFile | string) => 7 | typeof file === "string" ? file.endsWith(".md") : file.extension === "md"; 8 | 9 | /** 10 | * @param newName include extension 11 | * @returns null if given root dir 12 | */ 13 | export const getRenamedPath = (af: TAbstractFile, newName: string) => { 14 | const dir = getParentPath(af.path); 15 | return dir ? join(dir, newName) : dir; 16 | }; 17 | 18 | export const getParentPath = (src: string): string | null => { 19 | // if root dir given 20 | if (src === "/") return null; 21 | 22 | const path = dirname(src); 23 | if (path === ".") return "/"; 24 | else return path; 25 | }; 26 | 27 | /** Opreation on TAbstractFile */ 28 | export const afOp = ( 29 | target: TAbstractFile, 30 | fileOp: (file: TFile) => void, 31 | folderOp: (folder: TFolder) => void, 32 | ) => { 33 | if (target instanceof TFile) { 34 | fileOp(target); 35 | } else if (target instanceof TFolder) { 36 | folderOp(target); 37 | } else { 38 | log.error("unexpected TAbstractFile type", target); 39 | throw new Error("unexpected TAbstractFile type"); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aidenlx/folder-note-core", 3 | "version": "1.3.6", 4 | "description": "Provide core features and API for folder notes", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib/**/*" 9 | ], 10 | "type": "module", 11 | "packageManager": "yarn@3.1.1", 12 | "scripts": { 13 | "dev": "cross-env BUILD=dev node esbuild.config.mjs", 14 | "check": "tsc --noEmit", 15 | "prelib": "rm -rf lib && node ./scripts/cp-dts.mjs", 16 | "lib": "tsc --project tsconfig-lib.json", 17 | "build": "yarn run lib && cross-env BUILD=production node esbuild.config.mjs", 18 | "prettier": "prettier --write 'src/**/*.+(ts|tsx|json|html|css)'", 19 | "eslint": "eslint . --ext .ts,.tsx --fix", 20 | "release-ob": "release-it --config .release-it.ob.cjs", 21 | "release-full": "release-it", 22 | "release-lib": "release-it --config .release-it.lib.cjs" 23 | }, 24 | "keywords": [], 25 | "author": "AidenLx", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@aidenlx/esbuild-plugin-obsidian": "^0.1.4", 29 | "@aidenlx/eslint-config": "^0.1.2", 30 | "@aidenlx/obsidian-plugin-bumper": "^0.1.3", 31 | "@aidenlx/prettier-config": "^0.1.0", 32 | "@aidenlx/release-it-config": "^0.1.11", 33 | "@aidenlx/ts-config": "^0.1.2", 34 | "@release-it/bumper": "^3.0.1", 35 | "@release-it/conventional-changelog": "^4.1.0", 36 | "@types/json-schema": "^7.0.9", 37 | "@types/node": "^17.0.18", 38 | "@typescript-eslint/eslint-plugin": "^5.12.0", 39 | "@typescript-eslint/parser": "^5.12.0", 40 | "assert-never": "^1.2.1", 41 | "conventional-changelog-angular": "^5.0.13", 42 | "cross-env": "^7.0.3", 43 | "cz-conventional-changelog": "^3.3.0", 44 | "esbuild": "^0.14.23", 45 | "eslint": "^8.9.0", 46 | "eslint-config-prettier": "^8.4.0", 47 | "eslint-import-resolver-typescript": "^2.5.0", 48 | "eslint-plugin-import": "^2.25.4", 49 | "eslint-plugin-jsdoc": "^37.9.4", 50 | "eslint-plugin-prefer-arrow": "^1.2.3", 51 | "eslint-plugin-prettier": "^4.0.0", 52 | "eslint-plugin-simple-import-sort": "^7.0.0", 53 | "json": "^11.0.0", 54 | "loglevel": "^1.8.0", 55 | "monkey-around": "^2.3.0", 56 | "obsidian": "latest", 57 | "path-browserify": "^1.0.1", 58 | "prettier": "^2.5.1", 59 | "release-it": "^14.12.4", 60 | "semver": "^7.3.5", 61 | "tslib": "^2.3.1", 62 | "typescript": "^4.5.5" 63 | }, 64 | "dependencies": { 65 | "loglevel": "^1.8.0", 66 | "obsidian": "latest" 67 | }, 68 | "browser": { 69 | "path": "path-browserify" 70 | }, 71 | "publishConfig": { 72 | "access": "public" 73 | }, 74 | "config": { 75 | "commitizen": { 76 | "path": "./node_modules/cz-conventional-changelog" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Folder Note Core 2 | 3 | Provide core features and API for [folder notes](https://github.com/aidenlx/alx-folder-note). 4 | 5 | - [create folder note](https://github.com/aidenlx/alx-folder-note/wiki/create-folder-note) easily, with [multiple preferences](https://github.com/aidenlx/alx-folder-note/wiki/folder-note-pref) and [template support](https://github.com/aidenlx/alx-folder-note/wiki/core-settings#template) 6 | - folder and folder note working as one 7 | - [folder and linked note synced as one](https://github.com/aidenlx/alx-folder-note/wiki/core-settings#auto-rename): change folder name from folder note; folder note moves with your folder 8 | - [delete folder within folder note](https://github.com/aidenlx/alx-folder-note/wiki/delete-folder-from-folder-note) 9 | 10 | ## How to use 11 | 12 | ### For users 13 | 14 | This plugin aimed to provide the following basic features for folder note: 15 | 16 | - commands and options in context menu for delete/create folder note 17 | - sync folder and folder note names and location 18 | 19 | For advanced feature such as file explorer patches and folder overviews, install this plugin with [alx-folder-note v0.11.0+](https://github.com/aidenlx/alx-folder-note) 20 | 21 | ### For developers 22 | 23 | 1. run `npm i -D @aidenlx/folder-note-core` in your plugin dir 24 | 2. import the api (add `import { getApi, isPluginEnabled, registerApi } from "@aidenlx/folder-note-core"`) 25 | 3. use api 26 | 1. check if enabled: `isPluginEnabled(YourPluginInstance)` 27 | 2. access api: `getApi()` / `getApi(YourPluginInstance)` 28 | 3. use api when it's ready: `registerApi(YourPluginInstance, (api)=>{// do something })` 29 | 4. bind to events: `YourPluginInstance.registerEvent(YourPluginInstance.app.vault.on("folder-note:...",(...)=>{...}))` 30 | 31 | ## Compatibility 32 | 33 | The required API feature is only available for Obsidian v0.13.24+. 34 | 35 | ## Installation 36 | 37 | ### From Obsidian 38 | 39 | 1. Open `Settings` > `Third-party plugin` 40 | 2. Make sure Safe mode is **off** 41 | 3. Click `Browse community plugins` 42 | 4. Search for this plugin 43 | 5. Click `Install` 44 | 6. Once installed, close the community plugins window and the patch is ready to use. 45 | 46 | ### From GitHub 47 | 48 | 1. Download the Latest Release from the Releases section of the GitHub Repository 49 | 2. Put files to your vault's plugins folder: `/.obsidian/plugins/folder-note-core` 50 | 3. Reload Obsidian 51 | 4. If prompted about Safe Mode, you can disable safe mode and enable the plugin. 52 | Otherwise, head to Settings, third-party plugins, make sure safe mode is off and 53 | enable the plugin from there. 54 | 55 | > Note: The `.obsidian` folder may be hidden. On macOS, you should be able to press `Command+Shift+Dot` to show the folder in Finder. 56 | -------------------------------------------------------------------------------- /src/fnc-main.ts: -------------------------------------------------------------------------------- 1 | import assertNever from "assert-never"; 2 | import { around } from "monkey-around"; 3 | import { App, Plugin, PluginManifest } from "obsidian"; 4 | 5 | import log from "./logger"; 6 | import { AddOptionsForFolder, AddOptionsForNote } from "./modules/commands"; 7 | import NoteResolver from "./modules/resolver"; 8 | import VaultHandler from "./modules/vault-handler"; 9 | import { DEFAULT_SETTINGS, FNCoreSettings, FNCoreSettingTab } from "./settings"; 10 | import API, { API_NAME, FNCEvents, getApi, NoteLoc } from "./typings/api"; 11 | 12 | const ALX_FOLDER_NOTE = "alx-folder-note"; 13 | const API_NAME: API_NAME extends keyof typeof window ? API_NAME : never = 14 | "FolderNoteAPIv0" as const; // this line will throw error if name out of sync 15 | export default class FNCore extends Plugin { 16 | settings: FNCoreSettings = DEFAULT_SETTINGS; 17 | vaultHandler = new VaultHandler(this); 18 | resolver = new NoteResolver(this); 19 | api: API; 20 | 21 | settingTab = new FNCoreSettingTab(this); 22 | 23 | constructor(app: App, manifest: PluginManifest) { 24 | super(app, manifest); 25 | log.setDefaultLevel("ERROR"); 26 | const plugin = this; 27 | this.api = getApi(plugin); 28 | (window[API_NAME] = this.api) && 29 | this.register(() => delete window[API_NAME]); 30 | this.trigger("folder-note:api-ready", this.api); 31 | 32 | // patch create new note location for outside-same 33 | this.register( 34 | around(app.fileManager, { 35 | // eslint-disable-next-line prefer-arrow/prefer-arrow-functions 36 | getNewFileParent(next) { 37 | // eslint-disable-next-line prefer-arrow/prefer-arrow-functions 38 | return function (sourcePath) { 39 | if (app.vault.getConfig("newFileLocation") === "current") { 40 | const pref = plugin.settings.folderNotePref; 41 | switch (pref) { 42 | case NoteLoc.Index: 43 | case NoteLoc.Inside: 44 | break; 45 | case NoteLoc.Outside: { 46 | const folder = plugin.resolver.getFolderFromNote(sourcePath); 47 | if (folder) return folder; 48 | break; 49 | } 50 | default: 51 | assertNever(pref); 52 | } 53 | } 54 | return next.call(app.fileManager, sourcePath); 55 | }; 56 | }, 57 | }), 58 | ); 59 | } 60 | 61 | async onload() { 62 | log.info("loading folder-note-core"); 63 | 64 | await this.loadSettings(); 65 | 66 | if (!this.app.plugins.enabledPlugins.has(ALX_FOLDER_NOTE)) 67 | this.addSettingTab(this.settingTab); 68 | 69 | AddOptionsForNote(this); 70 | AddOptionsForFolder(this); 71 | this.vaultHandler.registerEvent(); 72 | } 73 | 74 | trigger(...args: FNCEvents): void { 75 | const [name, ...data] = args; 76 | this.app.vault.trigger(name, ...data); 77 | } 78 | 79 | async loadSettings() { 80 | this.settings = { ...this.settings, ...(await this.loadData()) }; 81 | log.setLevel(this.settings.logLevel); 82 | } 83 | 84 | async saveSettings() { 85 | await this.saveData(this.settings); 86 | } 87 | 88 | getNewFolderNote: API["getNewFolderNote"] = (folder) => 89 | this.settings.folderNoteTemplate 90 | .replace(/{{FOLDER_NAME}}/g, folder.name) 91 | .replace(/{{FOLDER_PATH}}/g, folder.path); 92 | } 93 | -------------------------------------------------------------------------------- /src/modules/commands.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownView, Menu, TFile, TFolder } from "obsidian"; 2 | 3 | import FNCore from "../fnc-main"; 4 | import { NoteLoc } from "../typings/api"; 5 | 6 | /** Add Make doc folder note and delete linked folder command */ 7 | export const AddOptionsForNote = (plugin: FNCore) => { 8 | const { 9 | createFolderForNote, 10 | createFolderForNoteCheck, 11 | LinkToParentFolder, 12 | DeleteLinkedFolder, 13 | DeleteNoteAndLinkedFolder, 14 | } = plugin.resolver; 15 | 16 | plugin.addCommand({ 17 | id: "make-doc-folder-note", 18 | name: "Make current document folder note", 19 | checkCallback: (checking) => { 20 | const view = plugin.app.workspace.getActiveViewOfType(MarkdownView); 21 | if (checking) { 22 | return !!view && createFolderForNoteCheck(view.file); 23 | } else if (!!view) { 24 | createFolderForNote(view.file); 25 | } 26 | }, 27 | hotkeys: [], 28 | }); 29 | plugin.addCommand({ 30 | id: "link-to-parent-folder", 31 | name: "Link to Parent Folder", 32 | checkCallback: (checking) => { 33 | const view = plugin.app.workspace.getActiveViewOfType(MarkdownView); 34 | return !!view && LinkToParentFolder(view.file, checking); 35 | }, 36 | hotkeys: [], 37 | }); 38 | plugin.addCommand({ 39 | id: "delete-linked-folder", 40 | name: "Delete linked folder", 41 | checkCallback: (checking) => { 42 | const view = plugin.app.workspace.getActiveViewOfType(MarkdownView); 43 | return !!view && DeleteLinkedFolder(view.file, checking); 44 | }, 45 | hotkeys: [], 46 | }); 47 | plugin.addCommand({ 48 | id: "delete-with-linked-folder", 49 | name: "Delete note and linked folder", 50 | checkCallback: (checking) => { 51 | const view = plugin.app.workspace.getActiveViewOfType(MarkdownView); 52 | return !!view && DeleteNoteAndLinkedFolder(view.file, checking); 53 | }, 54 | hotkeys: [], 55 | }); 56 | plugin.registerEvent( 57 | plugin.app.workspace.on("file-menu", async (menu, af, source) => { 58 | if (af instanceof TFile && af.extension === "md") { 59 | if (LinkToParentFolder(af, true)) { 60 | menu.addItem((item) => 61 | item 62 | .setIcon("link") 63 | .setTitle("Link to Parent Folder") 64 | .onClick(() => LinkToParentFolder(af)), 65 | ); 66 | } 67 | if (await createFolderForNote(af, true)) { 68 | menu.addItem((item) => 69 | item 70 | .setIcon("create-new") 71 | .setTitle("Make Doc Folder Note") 72 | .onClick(() => { 73 | createFolderForNote(af); 74 | plugin.app.workspace.openLinkText(af.path, "", false); 75 | }), 76 | ); 77 | } 78 | if ( 79 | source !== "link-context-menu" && 80 | DeleteNoteAndLinkedFolder(af, true) 81 | ) { 82 | menu.addItem((item) => 83 | item 84 | .setIcon("trash") 85 | .setTitle("Delete Note and Linked Folder") 86 | .onClick(() => DeleteNoteAndLinkedFolder(af)), 87 | ); 88 | } 89 | } 90 | }), 91 | ); 92 | }; 93 | 94 | export const AddOptionsForFolder = (plugin: FNCore) => { 95 | const { 96 | OpenFolderNote, 97 | DeleteFolderNote, 98 | CreateFolderNote, 99 | DeleteNoteAndLinkedFolder, 100 | } = plugin.resolver; 101 | plugin.registerEvent( 102 | plugin.app.workspace.on("file-menu", (menu, af, source) => { 103 | if (af instanceof TFolder) { 104 | if (OpenFolderNote(af, true)) { 105 | menu.addItem((item) => 106 | item 107 | .setIcon("enter") 108 | .setTitle("Open Folder Note") 109 | .onClick(() => OpenFolderNote(af)), 110 | ); 111 | } 112 | if (DeleteFolderNote(af, true)) { 113 | menu.addItem((item) => 114 | item 115 | .setIcon("trash") 116 | .setTitle("Delete Folder Note") 117 | .onClick(() => DeleteFolderNote(af)), 118 | ); 119 | } 120 | if ( 121 | plugin.settings.folderNotePref === NoteLoc.Outside && 122 | plugin.settings.deleteOutsideNoteWithFolder === false && 123 | DeleteNoteAndLinkedFolder(af, true) 124 | ) 125 | menu.addItem((item) => 126 | item 127 | .setIcon("trash") 128 | .setTitle("Delete Folder and Folder Note") 129 | .onClick(() => DeleteNoteAndLinkedFolder(af)), 130 | ); 131 | if (CreateFolderNote(af, true)) 132 | menu.addItem((item) => 133 | item 134 | .setIcon("create-new") 135 | .setTitle("Create Folder Note") 136 | .onClick(() => CreateFolderNote(af)), 137 | ); 138 | } 139 | }), 140 | ); 141 | }; 142 | -------------------------------------------------------------------------------- /src/typings/api.ts: -------------------------------------------------------------------------------- 1 | import { OpenViewState, TFile, TFolder } from "obsidian"; 2 | 3 | import FNCore from "../fnc-main"; 4 | import { FNCoreSettings } from "../settings"; 5 | 6 | interface OldConfig { 7 | /** 8 | * Index=0, Inside=1, Outside=2, 9 | */ 10 | folderNotePref: 0 | 1 | 2; 11 | deleteOutsideNoteWithFolder: boolean; 12 | indexName: string; 13 | autoRename: boolean; 14 | folderNoteTemplate: string; 15 | } 16 | 17 | export enum NoteLoc { 18 | Index, 19 | Inside, 20 | Outside, 21 | } 22 | 23 | export type FolderNotePath = { 24 | /** the parent directory */ 25 | dir: string; 26 | /** the file name (including extension) */ 27 | name: string; 28 | /** full filepath that can be used to get TFile */ 29 | path: string; 30 | }; 31 | export default interface FolderNoteAPI { 32 | importSettings(settings: Partial): void; 33 | renderCoreSettings(target: HTMLElement): void; 34 | renderLogLevel(targer: HTMLElement): void; 35 | 36 | getFolderFromNote(note: TFile | string, strategy?: NoteLoc): TFolder | null; 37 | /** 38 | * Get path of given note/notePath's folder based on setting 39 | * @param note notePath or note TFile 40 | * @param newFolder if the path is used to create new folder 41 | * @returns folder path, will return null if note basename invaild and newFolder=false 42 | */ 43 | getFolderPath( 44 | note: TFile | string, 45 | newFolder: boolean, 46 | strategy?: NoteLoc, 47 | ): string | null; 48 | 49 | getFolderNote(folder: TFolder | string, strategy?: NoteLoc): TFile | null; 50 | /** Get the path to the folder note for given file based on setting, 51 | * @returns not guaranteed to exists */ 52 | getFolderNotePath( 53 | folder: TFolder | string, 54 | strategy?: NoteLoc, 55 | ): FolderNotePath | null; 56 | 57 | /** Generate folder note content for given folder based on template */ 58 | getNewFolderNote(folder: TFolder): string; 59 | 60 | OpenFolderNote( 61 | folder: TFolder | string, 62 | dryrun?: boolean, 63 | config?: { newLeaf?: boolean; openViewState?: OpenViewState }, 64 | ): boolean; 65 | /** 66 | * @returns return false if no linked folder found 67 | */ 68 | DeleteLinkedFolder(file: TFile, dryrun?: boolean): boolean; 69 | /** 70 | * Link current note to parent folder, move given file if needed 71 | * @returns return false if already linked 72 | */ 73 | LinkToParentFolder(file: TFile, dryrun?: boolean): boolean; 74 | /** 75 | * Delete given file as well as linked folder, will prompt for confirmation 76 | * @returns return false if no linked folder found 77 | */ 78 | DeleteNoteAndLinkedFolder(target: TFile | TFolder, dryrun?: boolean): boolean; 79 | /** 80 | * Create folder based on config and move given file if needed 81 | * @returns return false if folder already exists 82 | */ 83 | createFolderForNote(file: TFile, dryrun?: boolean): Promise; 84 | /** 85 | * @returns return false if folder note not exists 86 | */ 87 | DeleteFolderNote(folder: TFolder, dryrun?: boolean): boolean; 88 | /** 89 | * @returns return false if folder note already exists 90 | */ 91 | CreateFolderNote(folder: TFolder, dryrun?: boolean): boolean; 92 | } 93 | 94 | declare global { 95 | // Must use var, no const/let 96 | var FolderNoteAPIv0: FolderNoteAPI | undefined; 97 | } 98 | export type API_NAME = "FolderNoteAPIv0"; 99 | 100 | export type FNCEvents = 101 | | [name: "folder-note:api-ready", api: FolderNoteAPI] 102 | | [name: "folder-note:cfg-changed"] 103 | | [name: "folder-note:delete", note: TFile, folder: TFolder] 104 | | [ 105 | name: "folder-note:rename", 106 | note: [file: TFile, oldPath: string], 107 | folder: [folder: TFolder, oldPath: string], 108 | ] 109 | | [name: "folder-note:create", note: TFile, folder: TFolder]; 110 | 111 | export const getApi = (plugin: FNCore): FolderNoteAPI => { 112 | return { 113 | get renderCoreSettings() { 114 | return plugin.settingTab.renderCoreSettings; 115 | }, 116 | get renderLogLevel() { 117 | return plugin.settingTab.setLogLevel; 118 | }, 119 | importSettings: (cfg) => { 120 | if (cfg.folderNotePref !== undefined) { 121 | switch (cfg.folderNotePref) { 122 | case 0: 123 | cfg.folderNotePref = NoteLoc.Index; 124 | break; 125 | case 1: 126 | cfg.folderNotePref = NoteLoc.Inside; 127 | break; 128 | case 2: 129 | cfg.folderNotePref = NoteLoc.Outside; 130 | break; 131 | default: 132 | break; 133 | } 134 | let toImport = Object.fromEntries( 135 | Object.entries(cfg).filter(([k, v]) => v !== undefined), 136 | ) as FNCoreSettings; 137 | 138 | plugin.settings = { ...plugin.settings, ...toImport }; 139 | plugin.saveSettings(); 140 | } 141 | }, 142 | get getNewFolderNote() { 143 | return plugin.getNewFolderNote; 144 | }, 145 | get getFolderFromNote() { 146 | return plugin.resolver.getFolderFromNote; 147 | }, 148 | get getFolderPath() { 149 | return plugin.resolver.getFolderPath; 150 | }, 151 | get getFolderNote() { 152 | return plugin.resolver.getFolderNote; 153 | }, 154 | get getFolderNotePath() { 155 | return plugin.resolver.getFolderNotePath; 156 | }, 157 | get DeleteLinkedFolder() { 158 | return plugin.resolver.DeleteLinkedFolder; 159 | }, 160 | get LinkToParentFolder() { 161 | return plugin.resolver.LinkToParentFolder; 162 | }, 163 | get DeleteNoteAndLinkedFolder() { 164 | return plugin.resolver.DeleteNoteAndLinkedFolder; 165 | }, 166 | get createFolderForNote() { 167 | return plugin.resolver.createFolderForNote; 168 | }, 169 | get DeleteFolderNote() { 170 | return plugin.resolver.DeleteFolderNote; 171 | }, 172 | get CreateFolderNote() { 173 | return plugin.resolver.CreateFolderNote; 174 | }, 175 | get OpenFolderNote() { 176 | return plugin.resolver.OpenFolderNote; 177 | }, 178 | }; 179 | }; 180 | -------------------------------------------------------------------------------- /src/modules/vault-handler.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FileManager, 3 | Notice, 4 | TAbstractFile, 5 | TFile, 6 | TFolder, 7 | Vault, 8 | } from "obsidian"; 9 | import { basename, dirname } from "path"; 10 | 11 | import FNCore from "../fnc-main"; 12 | import { afOp, getRenamedPath, isMd } from "../misc"; 13 | import { NoteLoc } from "../typings/api"; 14 | 15 | export default class VaultHandler { 16 | // @ts-ignore 17 | private on: Vault["on"] = (...args) => this.plugin.app.vault.on(...args); 18 | private delete: Vault["delete"] = (...args) => 19 | this.plugin.app.vault.delete(...args); 20 | private rename: FileManager["renameFile"] = (...args) => 21 | this.plugin.app.fileManager.renameFile(...args); 22 | 23 | private get settings() { 24 | return this.plugin.settings; 25 | } 26 | private get finder() { 27 | return this.plugin.resolver; 28 | } 29 | plugin: FNCore; 30 | 31 | constructor(plugin: FNCore) { 32 | this.plugin = plugin; 33 | } 34 | 35 | registerEvent = () => { 36 | this.plugin.registerEvent(this.on("create", this.onChange)); 37 | this.plugin.registerEvent(this.on("rename", this.onChange)); 38 | this.plugin.registerEvent(this.on("delete", this.onDelete)); 39 | }; 40 | 41 | private shouldRename(af: TAbstractFile, oldPath?: string): oldPath is string { 42 | if (!this.settings.autoRename || !oldPath) return false; 43 | const renameOnly = 44 | this.settings.folderNotePref !== NoteLoc.Index && 45 | dirname(af.path) === dirname(oldPath) // rename only, same loc 46 | ? true 47 | : false; 48 | // sync loc is enabled in folder renames only 49 | const syncLoc = 50 | af instanceof TFolder && 51 | this.settings.folderNotePref === NoteLoc.Outside && 52 | dirname(af.path) !== dirname(oldPath) 53 | ? true 54 | : false; 55 | return renameOnly || syncLoc; 56 | } 57 | 58 | onChange = (af: TAbstractFile, oldPath?: string) => { 59 | const { getFolderNote, getFolderFromNote, getFolderNotePath } = this.finder; 60 | 61 | function getOldLinked(af: TFile): TFolder | null; 62 | function getOldLinked(af: TFolder): TFile | null; 63 | function getOldLinked(af: TAbstractFile): TFile | TFolder | null; 64 | // eslint-disable-next-line prefer-arrow/prefer-arrow-functions 65 | function getOldLinked(af: TAbstractFile): TFile | TFolder | null { 66 | if (af instanceof TFolder) { 67 | return oldPath ? getFolderNote(oldPath) : null; 68 | } else if (af instanceof TFile) { 69 | return oldPath && isMd(oldPath) ? getFolderFromNote(oldPath) : null; 70 | } else return null; 71 | } 72 | 73 | function getLinked(af: TFile): TFolder | null; 74 | function getLinked(af: TFolder): TFile | null; 75 | // eslint-disable-next-line prefer-arrow/prefer-arrow-functions 76 | function getLinked(af: TAbstractFile): TFile | TFolder | null { 77 | if (af instanceof TFolder) { 78 | return getFolderNote(af); 79 | } else if (af instanceof TFile) { 80 | return getFolderFromNote(af); 81 | } else return null; 82 | } 83 | 84 | // markdown <-> non-md 85 | if (oldPath && af instanceof TFile && isMd(oldPath) !== isMd(af)) { 86 | const oldLinked = getOldLinked(af); 87 | if (oldLinked) { 88 | // folder note -> non-md 89 | this.plugin.trigger("folder-note:delete", af, oldLinked); 90 | } else { 91 | // non-md -> md, check if folder note 92 | const nowLinked = getFolderFromNote(af); 93 | if (nowLinked) this.plugin.trigger("folder-note:create", af, nowLinked); 94 | } 95 | } else { 96 | // check if new location contains matched folder and mark if exists 97 | let newExists = false, 98 | linked; 99 | 100 | afOp( 101 | af, 102 | (file) => { 103 | linked = getLinked(file); 104 | if (linked) { 105 | newExists = true; 106 | this.plugin.trigger("folder-note:create", file, linked); 107 | } 108 | }, 109 | (folder) => { 110 | linked = getLinked(folder); 111 | if (linked) { 112 | newExists = true; 113 | this.plugin.trigger("folder-note:create", linked, folder); 114 | } 115 | }, 116 | ); 117 | 118 | // onRename, check if oldPath has any folder note/linked folder 119 | const oldLinked = getOldLinked(af); 120 | if (!oldLinked) return; 121 | 122 | const renameTo = 123 | af instanceof TFolder 124 | ? getFolderNotePath(af)?.path ?? "" 125 | : af instanceof TFile 126 | ? getRenamedPath(oldLinked, af.basename) ?? "" 127 | : ""; 128 | 129 | if (this.shouldRename(af, oldPath)) 130 | if (!newExists && renameTo) { 131 | this.rename(oldLinked, renameTo); 132 | afOp( 133 | af, 134 | (f) => 135 | this.plugin.trigger( 136 | "folder-note:rename", 137 | [f, oldPath], 138 | [oldLinked as TFolder, renameTo], 139 | ), 140 | (f) => 141 | this.plugin.trigger( 142 | "folder-note:rename", 143 | [oldLinked as TFile, renameTo], 144 | [f, oldPath], 145 | ), 146 | ); 147 | return; 148 | } else { 149 | const target = 150 | oldLinked instanceof TFile ? "folder note" : "linked folder", 151 | baseMessage = `Failed to sync name of ${target}: `, 152 | errorMessage = newExists 153 | ? `${target} ${basename(renameTo)} already exists` 154 | : "check console for more details"; 155 | new Notice(baseMessage + errorMessage); 156 | } 157 | 158 | // reset old linked folder note/folder mark when no rename is performed 159 | afOp( 160 | af, 161 | (f) => 162 | this.plugin.trigger("folder-note:delete", f, oldLinked as TFolder), 163 | (f) => this.plugin.trigger("folder-note:delete", oldLinked as TFile, f), 164 | ); 165 | } 166 | }; 167 | onDelete = (af: TAbstractFile) => { 168 | const { getFolderNote, getFolderFromNote } = this.finder; 169 | if (af instanceof TFolder) { 170 | const oldNote = getFolderNote(af); 171 | if (!(this.settings.folderNotePref === NoteLoc.Outside && oldNote)) 172 | return; 173 | 174 | if (this.settings.deleteOutsideNoteWithFolder) { 175 | this.delete(oldNote); 176 | } else this.plugin.trigger("folder-note:delete", oldNote, af); 177 | } else if (af instanceof TFile && isMd(af)) { 178 | const oldFolder = getFolderFromNote(af); 179 | if (oldFolder) this.plugin.trigger("folder-note:delete", af, oldFolder); 180 | } 181 | }; 182 | } 183 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.3.6](https://github.com/aidenlx/folder-note-core/compare/1.3.5...1.3.6) (2022-05-16) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * fix typescript error caused by out-of-date obsidian version ([7fcd15a](https://github.com/aidenlx/folder-note-core/commit/7fcd15a29da33b7807edf3c6a385c1aab9e61471)) 7 | 8 | ## [1.3.5](https://github.com/aidenlx/folder-note-core/compare/1.3.4...1.3.5) (2022-04-24) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * fix command check fail when active view not markdown ([ec0a84c](https://github.com/aidenlx/folder-note-core/commit/ec0a84c78c1a8c8fc42d839e118f460968dbe781)) 14 | 15 | ## [1.3.4](https://github.com/aidenlx/folder-note-core/compare/1.3.3...1.3.4) (2022-03-14) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * update links when perform "Make Doc Folder Note" and "Link to Parent Folder" ([#14](https://github.com/aidenlx/folder-note-core/issues/14)) ([c4e1046](https://github.com/aidenlx/folder-note-core/commit/c4e10464c808b473cc819879d32b319b209435cf)) 21 | 22 | ## [1.3.3](https://github.com/aidenlx/folder-note-core/compare/1.3.2...1.3.3) (2022-02-20) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * fix esbuild failure ([390342f](https://github.com/aidenlx/folder-note-core/commit/390342fc1171043ad2b49346e2ebc57cd2819b97)), closes [#11](https://github.com/aidenlx/folder-note-core/issues/11) 28 | 29 | ## [1.3.2](https://github.com/aidenlx/folder-note-core/compare/1.3.1...1.3.2) (2022-02-19) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * fix log level pollution to other plugins ([cff9774](https://github.com/aidenlx/folder-note-core/commit/cff977458d33e16f39910c7da1c2c7f0910f4a60)) 35 | * fix loglevel in setting not being applied on load ([fcfbca3](https://github.com/aidenlx/folder-note-core/commit/fcfbca335a09831687f1f449d7b13c63a2364260)) 36 | 37 | ## [1.3.1](https://github.com/aidenlx/folder-note-core/compare/1.3.1...1.3.2) (2021-11-23) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * **api:** fix OpenFolderNote not implement properly; add config param in OpenFolderNote ([e533095](https://github.com/aidenlx/folder-note-core/commit/e53309521101dec5f5d745cb7422e3dc0285389b)), closes [#6](https://github.com/aidenlx/folder-note-core/issues/6) 43 | 44 | 45 | 46 | # [1.3.0](https://github.com/aidenlx/folder-note-core/compare/1.3.1...1.3.2) (2021-11-21) 47 | 48 | 49 | ### Features 50 | 51 | * **api:** add menu item and api to open folder note of given folder ([11abe93](https://github.com/aidenlx/folder-note-core/commit/11abe93746eee15c76bbd360c26bfe6fbdd21df7)), closes [#3](https://github.com/aidenlx/folder-note-core/issues/3) 52 | * **resolver** fix createFolderForNote not working properly ([3f5a3cc](https://github.com/aidenlx/folder-note-core/commit/3f5a3cc910dfcc5861a8804f9af8709336a28632)) 53 | 54 | 55 | 56 | ## [1.2.6](https://github.com/aidenlx/folder-note-core/compare/1.2.5...1.2.6) (2021-11-18) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * **resolver:** fix createFolderForNote exec when already folder note in index&inside-same strategy ([68d2e73](https://github.com/aidenlx/folder-note-core/commit/68d2e73812121bc192cb9591c69d57376792c14a)) 62 | 63 | ## [1.2.5](https://github.com/aidenlx/folder-note-core/compare/1.2.4...1.2.5) (2021-09-15) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * **resolver:** fix getFolderPath failed to get filename when given path string ([39d0164](https://github.com/aidenlx/folder-note-core/commit/39d016474b7b741737070f6477fbfa8565130987)) 69 | 70 | ## [1.2.4](https://github.com/aidenlx/folder-note-core/compare/1.2.3...1.2.4) (2021-09-15) 71 | 72 | 73 | ### Bug Fixes 74 | 75 | * **resolver:** fix dot in folder path breaks folder note resolution ([6fdb8dd](https://github.com/aidenlx/folder-note-core/commit/6fdb8dd17054bb8ba119f44d4e74a8a9ebdfb5e0)) 76 | 77 | ## [1.2.3](https://github.com/aidenlx/folder-note-core/compare/1.2.2...1.2.3) (2021-09-12) 78 | 79 | 80 | ### Bug Fixes 81 | 82 | * **resolver:** fix fail to get folder note path when path contains dots ([ea9162e](https://github.com/aidenlx/folder-note-core/commit/ea9162e264a5e1ba5d49fb23c187bfc7bd6520c8)) 83 | 84 | ## [1.2.2](https://github.com/aidenlx/folder-note-core/compare/1.2.1...1.2.2) (2021-09-12) 85 | 86 | 87 | ### Features 88 | 89 | * **settings:** add option to set log level; expose renderLogLevel in api ([d284dd6](https://github.com/aidenlx/folder-note-core/commit/d284dd6cb8aa6536fa18748a2793c8783c27a8f5)) 90 | 91 | ## [1.2.1](https://github.com/aidenlx/folder-note-core/compare/1.2.0...1.2.1) (2021-09-12) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * update mobile flag ([84855ae](https://github.com/aidenlx/folder-note-core/commit/84855aed8e698b126c33d65e3ba48010d3e53839)) 97 | 98 | # [1.2.0](https://github.com/aidenlx/folder-note-core/compare/1.1.0...1.2.0) (2021-09-12) 99 | 100 | 101 | ### Features 102 | 103 | * settings: add batch convert between strategies; api: new option to specify strategy ([0fd5bf2](https://github.com/aidenlx/folder-note-core/commit/0fd5bf2408dade7bed194727d1ad4cd4c8ea984e)) 104 | 105 | # [1.1.0](https://github.com/aidenlx/folder-note-core/compare/1.0.2...1.1.0) (2021-09-12) 106 | 107 | 108 | ### Bug Fixes 109 | 110 | * **resolver:** getFolderNotePath no longer throw uncaught error when given path contains extension ([f86a0a5](https://github.com/aidenlx/folder-note-core/commit/f86a0a501cf07b5a5b9b4fa851624127148a7585)) 111 | 112 | 113 | ### Features 114 | 115 | * add file menu option to delete note with folder for linked folder ([4d86d46](https://github.com/aidenlx/folder-note-core/commit/4d86d467602676044b1895ad13f6ffb3f4620423)) 116 | * new note created in folder note is placed in linked folder properly ([02fa8bf](https://github.com/aidenlx/folder-note-core/commit/02fa8bff5eb54e932319d06bfc8db6b89762f0f9)) 117 | 118 | ## [1.0.2](https://github.com/aidenlx/folder-note-core/compare/1.0.1...1.0.2) (2021-09-07) 119 | 120 | 121 | ### Bug Fixes 122 | 123 | * **resolver:** remove notice on dryrun for createFolderForNote ([f2f2ddf](https://github.com/aidenlx/folder-note-core/commit/f2f2ddf1941798160a6deec16ffe3ca8cc516d6a)) 124 | 125 | ## [1.0.1](https://github.com/aidenlx/folder-note-core/compare/1.0.0...1.0.1) (2021-09-07) 126 | 127 | 128 | ### Bug Fixes 129 | 130 | * **commands:** remove source restriction for file-menu ([4b6d702](https://github.com/aidenlx/folder-note-core/commit/4b6d70241fa6d47715b87fd11d45750b3d7ab13c)) 131 | 132 | # [1.0.0](https://github.com/aidenlx/folder-note-core/compare/0.2.0...1.0.0) (2021-09-07) 133 | 134 | 135 | ### Bug Fixes 136 | 137 | * **commands:** fix createFolderForNote() check not using await ([7a26e60](https://github.com/aidenlx/folder-note-core/commit/7a26e60e2f17a7fb81ca497de24557db4bfcf97e)) 138 | * **resolver:** fix invaild path returned when given file/folder's parent is root dir ([140f1c4](https://github.com/aidenlx/folder-note-core/commit/140f1c469007b69a62b6f3301a278994d79803da)) 139 | * **settings:** move mod key setting back to alx-folder-note ([fec1f21](https://github.com/aidenlx/folder-note-core/commit/fec1f212f32a50cebbff1541ca69deccfd6797ff)) 140 | 141 | 142 | ### Features 143 | 144 | * **api:** add api ready event ([e6b4779](https://github.com/aidenlx/folder-note-core/commit/e6b47797ce276ce29e66f19d29ce1e7bcf9f15f7)) 145 | * **api:** add util lib for npm package ([6aadaf4](https://github.com/aidenlx/folder-note-core/commit/6aadaf45df0ea603f33b719608d1add5ff066ced)) 146 | * **api:** expose getNewFolderNote(), getFolderNotePath() and getFolderPath() ([9642cc6](https://github.com/aidenlx/folder-note-core/commit/9642cc6ff403e73e7aa14204baeff7e550c09858)) 147 | * **api:** update api import: no manual types.d.ts needed ([048342c](https://github.com/aidenlx/folder-note-core/commit/048342c0cb7e03d786f6553418f3fb5e5dc202dc)) 148 | 149 | 150 | ### BREAKING CHANGES 151 | 152 | * **api:** FolderNoteAPI is no longer a default export 153 | * **api:** getFolderNote() no longer accept second arg when given path; getFolderNote() and getFolderFromNote() will return null when given file invaild (detail provided in console) 154 | 155 | # [0.2.0](https://github.com/aidenlx/folder-note-core/compare/0.1.0...0.2.0) (2021-09-01) 156 | 157 | 158 | ### Features 159 | 160 | * **settings:** expose items in setting tabs in api; fix OldConfig def ([1b878d9](https://github.com/aidenlx/folder-note-core/commit/1b878d9ee8804eed8541fcac8ce59081166b2c39)) 161 | 162 | # 0.1.0 (2021-09-01) 163 | 164 | 165 | ### Features 166 | 167 | * **api:** import settings from alx-folder-note ([2375deb](https://github.com/aidenlx/folder-note-core/commit/2375debed8cb23a9727d76d5a3c34b5ace667101)) 168 | * migrate code from alx-folder-note ([64a6991](https://github.com/aidenlx/folder-note-core/commit/64a699159b8a21e94a7f965c4a2fc7f1c5f2af8a)) 169 | 170 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import type { LogLevelNumbers } from "loglevel"; 2 | import { 3 | ButtonComponent, 4 | debounce, 5 | DropdownComponent, 6 | Modal, 7 | Notice, 8 | PluginSettingTab, 9 | Setting, 10 | TextAreaComponent, 11 | TFile, 12 | TFolder, 13 | } from "obsidian"; 14 | 15 | import FNCore from "./fnc-main"; 16 | import log from "./logger"; 17 | import { FolderNotePath, NoteLoc } from "./typings/api"; 18 | 19 | export interface FNCoreSettings { 20 | folderNotePref: NoteLoc; 21 | deleteOutsideNoteWithFolder: boolean; 22 | indexName: string; 23 | autoRename: boolean; 24 | folderNoteTemplate: string; 25 | logLevel: LogLevelNumbers; 26 | } 27 | 28 | export const DEFAULT_SETTINGS: FNCoreSettings = { 29 | folderNotePref: NoteLoc.Inside, 30 | deleteOutsideNoteWithFolder: false, 31 | indexName: "_about_", 32 | autoRename: true, 33 | folderNoteTemplate: "# {{FOLDER_NAME}}", 34 | logLevel: 4, 35 | }; 36 | 37 | const LocDescMap: Record = { 38 | [NoteLoc.Index]: "Inside Folder, Index File", 39 | [NoteLoc.Inside]: "Inside Folder, With Same Name", 40 | [NoteLoc.Outside]: "Outside Folder, With Same Name", 41 | }; 42 | 43 | export class FNCoreSettingTab extends PluginSettingTab { 44 | constructor(public plugin: FNCore) { 45 | super(plugin.app, plugin); 46 | } 47 | 48 | display(): void { 49 | let { containerEl } = this; 50 | containerEl.empty(); 51 | this.renderCoreSettings(containerEl); 52 | } 53 | 54 | renderCoreSettings = (target: HTMLElement) => { 55 | this.setStrategy(target); 56 | if (this.plugin.settings.folderNotePref === NoteLoc.Index) 57 | this.setIndexName(target); 58 | else if (this.plugin.settings.folderNotePref === NoteLoc.Outside) 59 | this.setDeleteWithFolder(target); 60 | this.setTemplate(target); 61 | if (this.plugin.settings.folderNotePref !== NoteLoc.Index) 62 | this.setAutoRename(target); 63 | }; 64 | 65 | setLogLevel = (containerEl: HTMLElement) => { 66 | new Setting(containerEl) 67 | .setName("Log Level of folder-note-core") 68 | .setDesc("Change this options if debug is required") 69 | .addDropdown((dp) => 70 | dp 71 | .then((dp) => 72 | Object.entries(log.levels).forEach(([key, val]) => 73 | dp.addOption(val.toString(), key), 74 | ), 75 | ) 76 | .setValue(log.getLevel().toString()) 77 | .onChange(async (val) => { 78 | const level = +val as LogLevelNumbers; 79 | log.setLevel(level); 80 | this.plugin.settings.logLevel = level; 81 | await this.plugin.saveSettings(); 82 | }), 83 | ); 84 | }; 85 | 86 | setDeleteWithFolder = (containerEl: HTMLElement) => { 87 | new Setting(containerEl) 88 | .setName("Delete Outside Note with Folder") 89 | .setDesc( 90 | createFragment((el) => { 91 | el.appendText("Delete folder note outside when folder is deleted"); 92 | el.createDiv({ 93 | text: "Warning: The note will be deleted when the folder is moved outside of vault", 94 | cls: "mod-warning", 95 | }); 96 | }), 97 | ) 98 | .addToggle((toggle) => 99 | toggle 100 | .setValue(this.plugin.settings.deleteOutsideNoteWithFolder) 101 | .onChange(async (value) => { 102 | this.plugin.settings.deleteOutsideNoteWithFolder = value; 103 | await this.plugin.saveSettings(); 104 | }), 105 | ); 106 | }; 107 | setStrategy = (containerEl: HTMLElement) => { 108 | new Setting(containerEl) 109 | .setName("Note File Storage Strategy") 110 | .setDesc( 111 | createFragment((el) => { 112 | el.appendText( 113 | "Select how you would like the folder note to be stored", 114 | ); 115 | el.createEl("br"); 116 | el.createEl("a", { 117 | href: "https://github.com/aidenlx/alx-folder-note/wiki/folder-note-pref", 118 | text: "Check here", 119 | }); 120 | el.appendText( 121 | " for more detail for pros and cons for different strategies", 122 | ); 123 | }), 124 | ) 125 | .addDropdown((dropDown) => { 126 | dropDown 127 | .addOptions(LocDescMap) 128 | .setValue(this.plugin.settings.folderNotePref.toString()) 129 | .onChange(async (value: string) => { 130 | this.plugin.settings.folderNotePref = +value; 131 | this.plugin.trigger("folder-note:cfg-changed"); 132 | await this.plugin.saveSettings(); 133 | }); 134 | }); 135 | new Setting(containerEl) 136 | .setName("Switch Strategy") 137 | .setDesc( 138 | createFragment((el) => { 139 | el.appendText( 140 | "Batch convert existing folder notes to use new storage strategy", 141 | ); 142 | el.createDiv({ 143 | text: "Warning: This function is experimental and dangerous, make sure to fully backup the vault before the conversion", 144 | cls: "mod-warning", 145 | }); 146 | }), 147 | ) 148 | .addButton((cb) => 149 | cb 150 | .setTooltip("Open Dialog") 151 | .setIcon("popup-open") 152 | .setCta() 153 | .onClick(() => new SwitchStrategyDialog(this.plugin).open()), 154 | ); 155 | }; 156 | setIndexName = (containerEl: HTMLElement) => { 157 | new Setting(containerEl) 158 | .setName("Name for Index File") 159 | .setDesc("Set the note name to be recognized as index file for folders") 160 | .addText((text) => { 161 | const onChange = async (value: string) => { 162 | this.plugin.settings.indexName = value; 163 | this.plugin.trigger("folder-note:cfg-changed"); 164 | await this.plugin.saveSettings(); 165 | }; 166 | text 167 | .setValue(this.plugin.settings.indexName) 168 | .onChange(debounce(onChange, 500, true)); 169 | }); 170 | }; 171 | setTemplate = (containerEl: HTMLElement) => { 172 | new Setting(containerEl) 173 | .setName("Folder Note Template") 174 | .setDesc( 175 | createFragment((descEl) => { 176 | descEl.appendText("The template used to generate new folder note."); 177 | descEl.appendChild(document.createElement("br")); 178 | descEl.appendText("Supported placeholders:"); 179 | descEl.appendChild(document.createElement("br")); 180 | descEl.appendText("{{FOLDER_NAME}} {{FOLDER_PATH}}"); 181 | }), 182 | ) 183 | .addTextArea((text) => { 184 | const onChange = async (value: string) => { 185 | this.plugin.settings.folderNoteTemplate = value; 186 | await this.plugin.saveSettings(); 187 | }; 188 | text 189 | .setValue(this.plugin.settings.folderNoteTemplate) 190 | .onChange(debounce(onChange, 500, true)); 191 | text.inputEl.rows = 8; 192 | text.inputEl.cols = 30; 193 | }); 194 | }; 195 | setAutoRename = (containerEl: HTMLElement) => { 196 | new Setting(containerEl) 197 | .setName("Auto Sync") 198 | .setDesc("Keep name and location of folder note and folder in sync") 199 | .addToggle((toggle) => { 200 | toggle.setValue(this.plugin.settings.autoRename); 201 | toggle.onChange(async (value) => { 202 | this.plugin.settings.autoRename = value; 203 | await this.plugin.saveSettings(); 204 | }); 205 | }); 206 | }; 207 | } 208 | 209 | class SwitchStrategyDialog extends Modal { 210 | buttonContainerEl: HTMLDivElement; 211 | outputEl: TextAreaComponent; 212 | fromOptsEl: DropdownComponent; 213 | toOptsEl: DropdownComponent; 214 | 215 | constructor(public plugin: FNCore) { 216 | super(plugin.app); 217 | 218 | // Dropdowns 219 | this.fromOptsEl = new DropdownComponent( 220 | this.titleEl.createDiv({ text: "From: " }), 221 | ).addOptions(LocDescMap); 222 | this.toOptsEl = new DropdownComponent( 223 | this.titleEl.createDiv({ text: "To: " }), 224 | ).addOptions(LocDescMap); 225 | 226 | // Console output 227 | this.outputEl = new TextAreaComponent(this.contentEl) 228 | .setValue("Hello world") 229 | .setDisabled(true) 230 | .then((cb) => { 231 | cb.inputEl.style.width = "100%"; 232 | cb.inputEl.rows = 10; 233 | }); 234 | 235 | // Buttons 236 | this.buttonContainerEl = this.modalEl.createDiv({ 237 | cls: "modal-button-container", 238 | }); 239 | this.addButton((cb) => 240 | cb.setButtonText("Check Conflicts").onClick(() => this.Convert(true)), 241 | ); 242 | this.addButton((cb) => 243 | cb 244 | .setButtonText("Convert") 245 | .setWarning() 246 | .onClick(() => this.Convert()), 247 | ); 248 | this.addButton((cb) => 249 | cb.setButtonText("Cancel").onClick(this.close.bind(this)), 250 | ); 251 | } 252 | 253 | private addButton(cb: (component: ButtonComponent) => any): ButtonComponent { 254 | const button = new ButtonComponent(this.buttonContainerEl); 255 | cb(button); 256 | return button; 257 | } 258 | private log(message: string) { 259 | this.outputEl.setValue(this.outputEl.getValue() + "\n" + message); 260 | } 261 | private clear() { 262 | this.outputEl.setValue(""); 263 | } 264 | 265 | Convert = async (dryrun = false): Promise => { 266 | const { From, To } = this; 267 | this.clear(); 268 | if (From === null || To === null) { 269 | new Notice("Please select the strategies to convert from/to first"); 270 | } else if (From === To) { 271 | new Notice("Convert between same strategy, skipping..."); 272 | } else { 273 | const { getFolderNote, getFolderNotePath } = this.plugin.resolver; 274 | const folderNotes = this.app.vault 275 | .getAllLoadedFiles() 276 | .filter((af): af is TFolder => af instanceof TFolder && !af.isRoot()) 277 | .map((folder): [note: TFile, newPath: FolderNotePath] | null => { 278 | const note = getFolderNote(folder, From), 279 | newPath = note ? getFolderNotePath(folder, To) : null; 280 | if (note && newPath) { 281 | return [note, newPath]; 282 | } else { 283 | return null; 284 | } 285 | }); 286 | let isConflict = false; 287 | for (const iterator of folderNotes) { 288 | if (!iterator) continue; 289 | const [src, newPath] = iterator; 290 | if (await this.app.vault.exists(newPath.path)) { 291 | isConflict || (isConflict = true); 292 | this.log( 293 | `Unable to move file ${src.path}: file exist in ${newPath.path}`, 294 | ); 295 | } else if (!dryrun) { 296 | this.app.fileManager.renameFile(src, newPath.path); 297 | } 298 | } 299 | if (!isConflict) { 300 | if (dryrun) this.log("Check complete, no conflict found"); 301 | else this.log("Batch convert complete"); 302 | } 303 | } 304 | }; 305 | 306 | get From() { 307 | const val = this.fromOptsEl.getValue(); 308 | if (val && NoteLoc[+val]) return +val as NoteLoc; 309 | else return null; 310 | } 311 | get To() { 312 | const val = this.toOptsEl.getValue(); 313 | if (val && NoteLoc[+val]) return +val as NoteLoc; 314 | else return null; 315 | } 316 | 317 | onOpen() { 318 | this.clear(); 319 | const pref = this.plugin.settings.folderNotePref.toString(); 320 | this.fromOptsEl.setValue(pref); 321 | this.toOptsEl.setValue(pref); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/modules/resolver.ts: -------------------------------------------------------------------------------- 1 | import assertNever from "assert-never"; 2 | import { Modal, Notice, OpenViewState, TFile, TFolder } from "obsidian"; 3 | import { basename as getBase, join } from "path"; 4 | 5 | import FNCore from "../fnc-main"; 6 | import log from "../logger"; 7 | import { getParentPath, isMd } from "../misc"; 8 | import API, { FolderNotePath, NoteLoc } from "../typings/api"; 9 | 10 | export default class NoteResolver { 11 | plugin: FNCore; 12 | constructor(plugin: FNCore) { 13 | this.plugin = plugin; 14 | } 15 | 16 | private get settings() { 17 | return this.plugin.settings; 18 | } 19 | private get vault() { 20 | return this.plugin.app.vault; 21 | } 22 | 23 | getFolderFromNote: API["getFolderFromNote"] = (note, strategy) => { 24 | if (!isMd(note)) return null; 25 | const folderPath = this.getFolderPath(note, false, strategy); 26 | if (!folderPath) return null; 27 | const folder = this.vault.getAbstractFileByPath(folderPath); 28 | if (folder && folder instanceof TFolder) return folder; 29 | else return null; 30 | }; 31 | /** 32 | * Get path of given note/notePath's folder based on setting 33 | * @param note notePath or note TFile 34 | * @param newFolder if true, return folder in the same folder as note 35 | * @returns folder path, will return null if note basename invaild and newFolder=false 36 | */ 37 | getFolderPath: API["getFolderPath"] = ( 38 | note, 39 | newFolder = false, 40 | strategy?: NoteLoc, 41 | ) => { 42 | if (strategy === undefined) strategy = this.settings.folderNotePref; 43 | if (!isMd(note)) { 44 | log.info("getFolderPath(%o): given file not markdown", note); 45 | return null; 46 | } 47 | let parent: string, base: string; 48 | if (note instanceof TFile) { 49 | base = note.basename; 50 | parent = getParentPath(note.path) ?? ""; 51 | } else { 52 | base = getBase(note).slice(0, -3); // remove ending ".md" 53 | parent = getParentPath(note) ?? ""; 54 | } 55 | 56 | if (!parent) { 57 | log.info("getFolderPath(%o): no folder note for root dir", note); 58 | return null; 59 | } 60 | 61 | const getSiblingFolder = () => { 62 | if (parent === "/") return base; 63 | else return join(parent, base); 64 | }; 65 | switch (strategy) { 66 | case NoteLoc.Index: 67 | if (newFolder) return getSiblingFolder(); 68 | else if (base === this.settings.indexName) return parent; 69 | else { 70 | log.info("getFolderPath(%o): note name invaild", note); 71 | return null; 72 | } 73 | case NoteLoc.Inside: 74 | if (newFolder) return getSiblingFolder(); 75 | else if (base === getBase(parent)) return parent; 76 | else { 77 | log.info("getFolderPath(%o): note name invaild", note); 78 | return null; 79 | } 80 | case NoteLoc.Outside: { 81 | const dir = getSiblingFolder(); 82 | if (newFolder || base === getBase(dir)) return dir; 83 | else { 84 | log.info("getFolderPath(%o): note name invaild", note); 85 | return null; 86 | } 87 | } 88 | default: 89 | assertNever(strategy); 90 | } 91 | }; 92 | 93 | // Get Folder Note from Folder 94 | getFolderNote: API["getFolderNote"] = (folder, strategy) => 95 | this.findFolderNote(this.getFolderNotePath(folder, strategy)); 96 | findFolderNote = (info: FolderNotePath | null): TFile | null => { 97 | if (!info) return null; 98 | 99 | const note = this.vault.getAbstractFileByPath(info.path); 100 | if (note && note instanceof TFile) return note; 101 | else return null; 102 | }; 103 | getFolderNotePath: API["getFolderNotePath"] = (folder, strategy) => { 104 | if (strategy === undefined) strategy = this.settings.folderNotePref; 105 | 106 | const dirPath = typeof folder === "string" ? folder : folder.path, 107 | parent = getParentPath(dirPath); 108 | if (!parent) { 109 | // is root folder 110 | return null; 111 | } 112 | 113 | const { indexName } = this.settings; 114 | 115 | let dir: string, basename: string; 116 | switch (strategy) { 117 | case NoteLoc.Index: 118 | basename = indexName; 119 | dir = dirPath; 120 | break; 121 | case NoteLoc.Inside: 122 | basename = getBase(dirPath); 123 | dir = dirPath; 124 | break; 125 | case NoteLoc.Outside: 126 | basename = getBase(dirPath); 127 | dir = parent; 128 | break; 129 | default: 130 | assertNever(strategy); 131 | } 132 | 133 | return { 134 | dir, 135 | name: basename + ".md", 136 | path: dir === "/" ? basename + ".md" : join(dir, basename + ".md"), 137 | }; 138 | }; 139 | 140 | // Note Operations 141 | 142 | /** 143 | * @returns return false if no linked folder found 144 | */ 145 | DeleteLinkedFolder: API["DeleteLinkedFolder"] = ( 146 | file: TFile, 147 | dryrun = false, 148 | ): boolean => { 149 | if (!isMd(file)) return false; 150 | const folderResult = this.getFolderFromNote(file); 151 | if (folderResult && !dryrun) this.vault.delete(folderResult, true); 152 | return !!folderResult; 153 | }; 154 | /** 155 | * @returns return false if already linked 156 | */ 157 | LinkToParentFolder: API["LinkToParentFolder"] = ( 158 | file: TFile, 159 | dryrun = false, 160 | ): boolean => { 161 | if (!isMd(file)) return false; 162 | 163 | if (file.parent) { 164 | const fnPath = this.getFolderNotePath(file.parent), 165 | shouldRun = fnPath && !this.getFolderNote(file.parent); 166 | if (shouldRun && !dryrun) { 167 | const { path } = fnPath; 168 | this.plugin.app.fileManager.renameFile(file, path); 169 | } 170 | return !!shouldRun; 171 | } else return false; 172 | }; 173 | /** 174 | * @returns return false if file not folder note 175 | */ 176 | DeleteNoteAndLinkedFolder: API["DeleteNoteAndLinkedFolder"] = ( 177 | target, 178 | dryrun = false, 179 | ) => { 180 | let file: null | TFile, folder: null | TFolder; 181 | if (target instanceof TFile) { 182 | if (!isMd(target)) return false; 183 | file = target; 184 | folder = this.getFolderFromNote(target); 185 | } else { 186 | file = this.getFolderNote(target); 187 | folder = target; 188 | } 189 | 190 | if (file && folder && !dryrun) { 191 | new DeleteWarning(this.plugin, file, folder).open(); 192 | } 193 | return !!(file && folder); 194 | }; 195 | 196 | createFolderForNoteCheck = (file: TFile) => { 197 | const result = this._createFolderForNote(file); 198 | if (!result) return false; 199 | const { folderExist, newFolderPath } = result; 200 | return !!(!folderExist && newFolderPath); 201 | }; 202 | _createFolderForNote = (file: TFile) => { 203 | if (!isMd(file)) return null; 204 | 205 | const folderForNotePath = this.getFolderPath(file, false); 206 | if ( 207 | folderForNotePath && 208 | this.vault.getAbstractFileByPath(folderForNotePath) 209 | ) { 210 | log.info("createFolderForNote(%o): already folder note", file, file.path); 211 | return null; 212 | } 213 | 214 | const newFolderPath = this.getFolderPath(file, true), 215 | folderExist = 216 | newFolderPath && this.vault.getAbstractFileByPath(newFolderPath); 217 | return { newFolderPath, folderExist }; 218 | }; 219 | 220 | /** 221 | * @returns return false if folder already exists 222 | */ 223 | createFolderForNote: API["createFolderForNote"] = async ( 224 | file: TFile, 225 | dryrun = false, 226 | ): Promise => { 227 | const result = this._createFolderForNote(file); 228 | if (!result) return false; 229 | const { newFolderPath, folderExist } = result; 230 | if (folderExist) { 231 | log.info( 232 | "createFolderForNote(%o): target folder to create already exists", 233 | file, 234 | file.path, 235 | ); 236 | if (!dryrun) new Notice("Target folder to create already exists"); 237 | return false; 238 | } else if (!newFolderPath) { 239 | log.info( 240 | "createFolderForNote(%o): no vaild linked folder path for %s", 241 | file, 242 | file.path, 243 | ); 244 | if (!dryrun) new Notice("No vaild linked folder path for: " + file.path); 245 | } else if (!dryrun) { 246 | await this.vault.createFolder(newFolderPath); 247 | let newNotePath: string | null; 248 | switch (this.settings.folderNotePref) { 249 | case NoteLoc.Index: 250 | newNotePath = join(newFolderPath, this.settings.indexName + ".md"); 251 | break; 252 | case NoteLoc.Inside: 253 | newNotePath = join(newFolderPath, file.name); 254 | break; 255 | case NoteLoc.Outside: 256 | newNotePath = null; 257 | break; 258 | default: 259 | assertNever(this.settings.folderNotePref); 260 | } 261 | if (newNotePath) 262 | await this.plugin.app.fileManager.renameFile(file, newNotePath); 263 | } 264 | return !!(!folderExist && newFolderPath); 265 | }; 266 | 267 | // Folder Operations 268 | 269 | OpenFolderNote: API["OpenFolderNote"] = ( 270 | folder: TFolder | string, 271 | dryrun = false, 272 | config?: { newLeaf?: boolean; openViewState?: OpenViewState }, 273 | ) => { 274 | const noteResult = this.getFolderNote(folder); 275 | if (noteResult && !dryrun) { 276 | this.plugin.app.workspace.openLinkText( 277 | noteResult.path, 278 | "", 279 | config?.newLeaf, 280 | config?.openViewState, 281 | ); 282 | } 283 | return !!noteResult; 284 | }; 285 | /** 286 | * @returns return false if folder note not exists 287 | */ 288 | DeleteFolderNote: API["DeleteFolderNote"] = ( 289 | folder: TFolder, 290 | dryrun = false, 291 | ): boolean => { 292 | const noteResult = this.getFolderNote(folder); 293 | if (noteResult && !dryrun) this.vault.delete(noteResult); 294 | 295 | return !!noteResult; 296 | }; 297 | /** 298 | * @returns return false if folder note already exists 299 | */ 300 | CreateFolderNote: API["CreateFolderNote"] = ( 301 | folder: TFolder, 302 | dryrun = false, 303 | ): boolean => { 304 | let shouldRun, fnPath; 305 | if ( 306 | (shouldRun = 307 | !this.getFolderNote(folder) && 308 | (fnPath = this.getFolderNotePath(folder))) && 309 | !dryrun 310 | ) { 311 | this.vault.create(fnPath.path, this.plugin.getNewFolderNote(folder)); 312 | } 313 | return !!shouldRun; 314 | }; 315 | } 316 | 317 | class DeleteWarning extends Modal { 318 | target: TFile; 319 | targetFolder: TFolder; 320 | plugin: FNCore; 321 | constructor(plugin: FNCore, file: TFile, folder: TFolder) { 322 | super(plugin.app); 323 | this.plugin = plugin; 324 | this.target = file; 325 | this.targetFolder = folder; 326 | } 327 | 328 | get settings() { 329 | return this.plugin.settings; 330 | } 331 | 332 | deleteFolder() { 333 | let { contentEl } = this; 334 | contentEl.createEl("p", { 335 | text: "Warning: the entire folder and its content will be removed", 336 | cls: "mod-warning", 337 | }); 338 | const children = this.targetFolder.children.map((v) => v.name); 339 | contentEl.createEl("p", { 340 | text: 341 | children.length > 5 342 | ? children.slice(0, 5).join(", ") + "..." 343 | : children.join(", "), 344 | }); 345 | contentEl.createEl("p", { 346 | text: "Continue?", 347 | cls: "mod-warning", 348 | }); 349 | const buttonContainer = contentEl.createDiv({ 350 | cls: "modal-button-container", 351 | }); 352 | buttonContainer.createEl( 353 | "button", 354 | { text: "Yes", cls: "mod-warning" }, 355 | (el) => 356 | el.onClickEvent(() => { 357 | this.app.vault.delete(this.targetFolder, true); 358 | this.app.vault.delete(this.target); 359 | this.close(); 360 | }), 361 | ); 362 | buttonContainer.createEl("button", { text: "No" }, (el) => 363 | el.onClickEvent(() => { 364 | this.close(); 365 | }), 366 | ); 367 | } 368 | 369 | onOpen() { 370 | this.containerEl.addClass("warn"); 371 | this.deleteFolder(); 372 | } 373 | 374 | onClose() { 375 | let { contentEl } = this; 376 | contentEl.empty(); 377 | } 378 | } 379 | --------------------------------------------------------------------------------