├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── esbuild.config.mjs ├── manifest.json ├── package.json ├── src ├── FileInterface.ts ├── Setting.ts ├── main.ts ├── suggesters │ ├── FolderSuggester.ts │ └── suggest.ts └── uncover.ts ├── tsconfig.json └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | indent_style = tab 7 | indent_size = 4 8 | tab_width = 4 9 | end_of_line = lf 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | dst 3 | main.js 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier", 11 | ], 12 | parser: "@typescript-eslint/parser", 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | ecmaVersion: 13, 18 | sourceType: "module", 19 | }, 20 | plugins: ["react", "@typescript-eslint"], 21 | rules: { 22 | "no-console": process.env.NODE_ENV === "production" ? 2 : 0, 23 | "@typescript-eslint/explicit-module-boundary-types": 0, 24 | "no-unused-vars": ["error", { argsIgnorePattern: "^_" }], 25 | }, 26 | settings: { 27 | react: { 28 | version: "detect", 29 | }, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | .devcontainer 4 | 5 | # Dockerfile 6 | Dockerfile 7 | 8 | # npm 9 | node_modules 10 | package-lock.json 11 | 12 | # Don't include the compiled main.js file in the repo. 13 | # They should be uploaded to GitHub releases instead. 14 | main.js 15 | 16 | # Exclude sourcemaps 17 | *.map 18 | 19 | # obsidian 20 | data.json 21 | 22 | # other 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": true, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 qawatake 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Local File Interface Plugin 2 | 3 | This plugin provides commands for moving files in and out of the vault. 4 | There are four features as follows. 5 | 1. a command to import local files outside the vault, 6 | 2. a command to export files in the vault to other locations in the device, 7 | 3. a folder menu for import files, 8 | 4. a file menu for exporting. 9 | 10 | ### 1. Command to import local files outside the vault 11 | [![Image from Gyazo](https://i.gyazo.com/0d5c5a7831ff824091869c96b6f7da5c.gif)](https://gyazo.com/0d5c5a7831ff824091869c96b6f7da5c) 12 | 13 | ### 2. Command to export files in the vault to other locations in the device 14 | [![Image from Gyazo](https://i.gyazo.com/038d34f2511eee314dd5de89d7287e36.gif)](https://gyazo.com/038d34f2511eee314dd5de89d7287e36) 15 | 16 | ### 3. Folder menu for importing files 17 | [![Image from Gyazo](https://i.gyazo.com/d615c43e2bb0a000058fd2172e71e3bc.gif)](https://gyazo.com/d615c43e2bb0a000058fd2172e71e3bc) 18 | 19 | ### 4. File Menu for exporting 20 | [![Image from Gyazo](https://i.gyazo.com/1164f3141ae81ae9ac20e4b8f9c32e8d.gif)](https://gyazo.com/1164f3141ae81ae9ac20e4b8f9c32e8d) 21 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild'; 2 | import process from 'process'; 3 | import builtins from 'builtin-modules'; 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source, please visit the github repository of this plugin 8 | */ 9 | `; 10 | 11 | const prod = process.argv[2] === 'production'; 12 | 13 | esbuild 14 | .build({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ['src/main.ts'], 19 | bundle: true, 20 | external: ['obsidian', 'electron', ...builtins], 21 | format: 'cjs', 22 | watch: !prod, 23 | target: 'es2016', 24 | logLevel: 'info', 25 | sourcemap: prod ? false : 'inline', 26 | treeShaking: true, 27 | outfile: 'main.js', 28 | }) 29 | .catch(() => process.exit(1)); 30 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-local-file-interface-plugin", 3 | "name": "Local File Interface", 4 | "version": "0.1.0", 5 | "minAppVersion": "0.12.0", 6 | "description": "Provides commands for moving files in and out of the vault", 7 | "author": "qawatake", 8 | "authorUrl": "https://github.com/qawatake/obsidian-local-file-interface-plugin", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-sample-plugin", 3 | "version": "0.12.0", 4 | "description": "This is a sample plugin for Obsidian (https://obsidian.md)", 5 | "main": "main.js", 6 | "scripts": { 7 | "fix:prettier": "prettier --write src", 8 | "lint:prettier": "prettier --check src", 9 | "fix": "run-s fix:prettier fix:eslint", 10 | "fix:eslint": "eslint src --ext .ts --fix", 11 | "lint": "run-p lint:prettier lint:eslint", 12 | "lint:eslint": "eslint src --ext .ts", 13 | "dev": "node esbuild.config.mjs", 14 | "build": "node esbuild.config.mjs production" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "@types/node": "^17.0.5", 21 | "@typescript-eslint/eslint-plugin": "^5.8.1", 22 | "@typescript-eslint/parser": "^5.8.1", 23 | "builtin-modules": "^3.2.0", 24 | "esbuild": "~0.13.12", 25 | "eslint": "^8.5.0", 26 | "eslint-config-prettier": "^8.3.0", 27 | "eslint-plugin-react": "^7.28.0", 28 | "npm-run-all": "^4.1.5", 29 | "obsidian": "^0.13.11", 30 | "prettier": "^2.5.1", 31 | "tslib": "^2.3.1", 32 | "typescript": "^4.5.4" 33 | }, 34 | "dependencies": { 35 | "@popperjs/core": "^2.11.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/FileInterface.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | normalizePath, 4 | Notice, 5 | TFile, 6 | TFolder, 7 | moment, 8 | Platform, 9 | } from 'obsidian'; 10 | 11 | export class LocalFileInterfaceProvider { 12 | private app: App; 13 | 14 | constructor(app: App) { 15 | this.app = app; 16 | } 17 | 18 | async import(folder: TFolder): Promise { 19 | const inputEl = createEl('input', { type: 'file' }); 20 | 21 | inputEl.multiple = true; 22 | inputEl.addEventListener('change', async () => { 23 | const files = inputEl.files; 24 | if (files === null || files.length === 0) { 25 | return; 26 | } 27 | for (let i = 0; i < files.length; i++) { 28 | const file = files[i]; 29 | if (file === undefined) { 30 | continue; 31 | } 32 | const filepath = normalizePath(`${folder.path}/${file.name}`); 33 | 34 | // check coincidence 35 | const prefixOnConflict = 36 | this.app.vault.getAbstractFileByPath(filepath) instanceof 37 | TFile 38 | ? `CONFLICT_${moment().format('YYYY-MM-DD_HH-mm-ss')}_` 39 | : ''; 40 | 41 | const filepathWithoutConflict = normalizePath( 42 | `${folder.path}/${prefixOnConflict}${file.name}` 43 | ); 44 | 45 | try { 46 | await this.app.vault.createBinary( 47 | filepathWithoutConflict, 48 | await file.arrayBuffer() 49 | ); 50 | new Notice(`${file.name} imported!`); 51 | } catch (err) { 52 | console.log(`ERROR in Local File Interface: ${err}`); 53 | new Notice(`Failed to import ${file.name}`); 54 | } 55 | } 56 | 57 | inputEl.remove(); 58 | }); 59 | inputEl.click(); 60 | } 61 | 62 | async export(file: TFile): Promise { 63 | if (Platform.isDesktopApp) { 64 | this.exportInDesktop(file); 65 | } else if (Platform.isMobileApp) { 66 | this.exportInMobile(file); 67 | } else { 68 | console.log( 69 | 'ERROR in Local File Interface: unable to find platform type' 70 | ); 71 | } 72 | } 73 | 74 | private async exportInDesktop(file: TFile): Promise { 75 | const blob = new Blob([await this.app.vault.readBinary(file)]); 76 | const url = URL.createObjectURL(blob); 77 | const tmpDownloadEl = document.body.createEl('a', { 78 | href: url, 79 | }); 80 | tmpDownloadEl.download = file.name; 81 | tmpDownloadEl.click(); 82 | tmpDownloadEl.remove(); 83 | URL.revokeObjectURL(url); 84 | } 85 | 86 | private exportInMobile(file: TFile) { 87 | (this.app as any).openWithDefaultApp(file.path); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Setting.ts: -------------------------------------------------------------------------------- 1 | import LocalFileInterfacePlugin from 'main'; 2 | import { App, PluginSettingTab, Setting } from 'obsidian'; 3 | import { FolderSuggest } from 'suggesters/FolderSuggester'; 4 | 5 | export class LocalFileInterfacePluginSettingTab extends PluginSettingTab { 6 | plugin: LocalFileInterfacePlugin; 7 | 8 | constructor(app: App, plugin: LocalFileInterfacePlugin) { 9 | super(app, plugin); 10 | this.plugin = plugin; 11 | } 12 | 13 | display(): void { 14 | const { containerEl } = this; 15 | 16 | containerEl.empty(); 17 | 18 | new Setting(containerEl) 19 | .setName('Default location for imported items') 20 | .addSearch((component) => { 21 | new FolderSuggest(this.app, component.inputEl); 22 | component 23 | .setPlaceholder('Example: folder1/folder2') 24 | .setValue(this.plugin.settings.folder) 25 | .onChange((newFolder) => { 26 | this.plugin.settings.folder = newFolder; 27 | this.plugin.saveSettings(); 28 | }); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Notice, Plugin, Menu, TFile, TFolder, MenuItem } from 'obsidian'; 2 | import { LocalFileInterfaceProvider } from 'FileInterface'; 3 | import { LocalFileInterfacePluginSettingTab } from 'Setting'; 4 | 5 | interface LocalFileInterfacePluginSettings { 6 | folder: string; 7 | } 8 | 9 | const DEFAULT_SETTINGS: LocalFileInterfacePluginSettings = { 10 | folder: '/', 11 | }; 12 | 13 | export default class LocalFileInterfacePlugin extends Plugin { 14 | settings: LocalFileInterfacePluginSettings; 15 | fileInterfaceProvider: LocalFileInterfaceProvider; 16 | 17 | async onload() { 18 | await this.loadSettings(); 19 | this.fileInterfaceProvider = new LocalFileInterfaceProvider(this.app); 20 | 21 | // file menu: Import file here 22 | this.registerEvent( 23 | this.app.workspace.on( 24 | 'file-menu', 25 | (menu: Menu, abstractFile: TFile) => { 26 | if (!(abstractFile instanceof TFolder)) { 27 | return; 28 | } 29 | 30 | const folder = abstractFile; 31 | menu.addItem((item: MenuItem) => { 32 | item.setIcon('file-explorer-glyph') 33 | .setTitle('Import local files here') 34 | .onClick(() => { 35 | this.fileInterfaceProvider.import(folder); 36 | }); 37 | }); 38 | } 39 | ) 40 | ); 41 | 42 | // file memu: Export current file 43 | this.registerEvent( 44 | this.app.workspace.on('file-menu', (menu: Menu, file: TFile) => { 45 | if (!(file instanceof TFile)) { 46 | return; 47 | } 48 | menu.addItem((item: MenuItem) => { 49 | item.setIcon('file-explorer-glyph') 50 | .setTitle('Export out of the vault') 51 | .onClick(() => { 52 | this.fileInterfaceProvider.export(file); 53 | }); 54 | }); 55 | }) 56 | ); 57 | 58 | // commands 59 | this.addCommand({ 60 | id: 'local-file-interface-import', 61 | name: 'Import local files', 62 | callback: async () => { 63 | const folder = this.app.vault.getAbstractFileByPath( 64 | this.settings.folder 65 | ); 66 | if (!(folder instanceof TFolder)) { 67 | const errMsg = `ERROR in Local Folder Interface: ${this.settings.folder} is not a folder`; 68 | console.log(errMsg); 69 | new Notice( 70 | `ERROR in Local Folder Interface: ${this.settings.folder} is not a folder` 71 | ); 72 | return; 73 | } 74 | await this.fileInterfaceProvider.import(folder); 75 | }, 76 | }); 77 | 78 | this.addCommand({ 79 | id: 'local-file-interface-export', 80 | name: 'Export the current file', 81 | callback: async () => { 82 | const activeFile = this.app.workspace.getActiveFile(); 83 | if (activeFile === null) { 84 | return; 85 | } 86 | 87 | await this.fileInterfaceProvider.export(activeFile); 88 | }, 89 | }); 90 | 91 | this.addSettingTab( 92 | new LocalFileInterfacePluginSettingTab(this.app, this) 93 | ); 94 | } 95 | 96 | // onunload() {} 97 | 98 | async loadSettings() { 99 | this.settings = Object.assign( 100 | {}, 101 | DEFAULT_SETTINGS, 102 | await this.loadData() 103 | ); 104 | } 105 | 106 | async saveSettings() { 107 | await this.saveData(this.settings); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/suggesters/FolderSuggester.ts: -------------------------------------------------------------------------------- 1 | // Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes/blob/c8b1040f9d84ec8f4b8eae4782b23c2c6bf14e0e/src/ui/file-suggest.ts 2 | import { TAbstractFile, TFolder } from 'obsidian'; 3 | 4 | import { TextInputSuggest } from 'suggesters/suggest'; 5 | 6 | export class FolderSuggest extends TextInputSuggest { 7 | getSuggestions(inputStr: string): TFolder[] { 8 | const abstractFiles = this.app.vault.getAllLoadedFiles(); 9 | const folders: TFolder[] = []; 10 | const lowerCaseInputStr = inputStr.toLowerCase(); 11 | 12 | abstractFiles.forEach((folder: TAbstractFile) => { 13 | if ( 14 | folder instanceof TFolder && 15 | folder.path.toLowerCase().contains(lowerCaseInputStr) 16 | ) { 17 | folders.push(folder); 18 | } 19 | }); 20 | 21 | return folders; 22 | } 23 | 24 | renderSuggestion(file: TFolder, el: HTMLElement): void { 25 | el.setText(file.path); 26 | } 27 | 28 | selectSuggestion(file: TFolder): void { 29 | this.inputEl.value = file.path; 30 | this.inputEl.trigger('input'); 31 | this.close(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/suggesters/suggest.ts: -------------------------------------------------------------------------------- 1 | // Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes/blob/c8b1040f9d84ec8f4b8eae4782b23c2c6bf14e0e/src/ui/suggest.ts 2 | 3 | import { App, ISuggestOwner, Scope } from 'obsidian'; 4 | import { createPopper, Instance as PopperInstance } from '@popperjs/core'; 5 | 6 | const wrapAround = (value: number, size: number): number => { 7 | return ((value % size) + size) % size; 8 | }; 9 | 10 | class Suggest { 11 | private owner: ISuggestOwner; 12 | private values: T[]; 13 | private suggestions: HTMLDivElement[]; 14 | private selectedItem: number; 15 | private containerEl: HTMLElement; 16 | 17 | constructor( 18 | owner: ISuggestOwner, 19 | containerEl: HTMLElement, 20 | scope: Scope 21 | ) { 22 | this.owner = owner; 23 | this.containerEl = containerEl; 24 | 25 | containerEl.on( 26 | 'click', 27 | '.suggestion-item', 28 | this.onSuggestionClick.bind(this) 29 | ); 30 | containerEl.on( 31 | 'mousemove', 32 | '.suggestion-item', 33 | this.onSuggestionMouseover.bind(this) 34 | ); 35 | 36 | scope.register([], 'ArrowUp', (event) => { 37 | if (!event.isComposing) { 38 | this.setSelectedItem(this.selectedItem - 1, true); 39 | return false; 40 | } 41 | }); 42 | 43 | scope.register([], 'ArrowDown', (event) => { 44 | if (!event.isComposing) { 45 | this.setSelectedItem(this.selectedItem + 1, true); 46 | return false; 47 | } 48 | }); 49 | 50 | scope.register([], 'Enter', (event) => { 51 | if (!event.isComposing) { 52 | this.useSelectedItem(event); 53 | return false; 54 | } 55 | }); 56 | } 57 | 58 | onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void { 59 | event.preventDefault(); 60 | 61 | const item = this.suggestions.indexOf(el); 62 | this.setSelectedItem(item, false); 63 | this.useSelectedItem(event); 64 | } 65 | 66 | onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void { 67 | const item = this.suggestions.indexOf(el); 68 | this.setSelectedItem(item, false); 69 | } 70 | 71 | setSuggestions(values: T[]) { 72 | this.containerEl.empty(); 73 | const suggestionEls: HTMLDivElement[] = []; 74 | 75 | values.forEach((value) => { 76 | const suggestionEl = this.containerEl.createDiv('suggestion-item'); 77 | this.owner.renderSuggestion(value, suggestionEl); 78 | suggestionEls.push(suggestionEl); 79 | }); 80 | 81 | this.values = values; 82 | this.suggestions = suggestionEls; 83 | this.setSelectedItem(0, false); 84 | } 85 | 86 | useSelectedItem(event: MouseEvent | KeyboardEvent) { 87 | const currentValue = this.values[this.selectedItem]; 88 | if (currentValue) { 89 | this.owner.selectSuggestion(currentValue, event); 90 | } 91 | } 92 | 93 | setSelectedItem(selectedIndex: number, scrollIntoView: boolean) { 94 | const normalizedIndex = wrapAround( 95 | selectedIndex, 96 | this.suggestions.length 97 | ); 98 | const prevSelectedSuggestion = this.suggestions[this.selectedItem]; 99 | const selectedSuggestion = this.suggestions[normalizedIndex]; 100 | 101 | prevSelectedSuggestion?.removeClass('is-selected'); 102 | selectedSuggestion?.addClass('is-selected'); 103 | 104 | this.selectedItem = normalizedIndex; 105 | 106 | if (scrollIntoView) { 107 | selectedSuggestion.scrollIntoView(false); 108 | } 109 | } 110 | } 111 | 112 | export abstract class TextInputSuggest implements ISuggestOwner { 113 | protected app: App; 114 | protected inputEl: HTMLInputElement | HTMLTextAreaElement; 115 | 116 | private popper: PopperInstance; 117 | private scope: Scope; 118 | private suggestEl: HTMLElement; 119 | private suggest: Suggest; 120 | 121 | constructor(app: App, inputEl: HTMLInputElement | HTMLTextAreaElement) { 122 | this.app = app; 123 | this.inputEl = inputEl; 124 | this.scope = new Scope(); 125 | 126 | this.suggestEl = createDiv('suggestion-container'); 127 | const suggestion = this.suggestEl.createDiv('suggestion'); 128 | this.suggest = new Suggest(this, suggestion, this.scope); 129 | 130 | this.scope.register([], 'Escape', this.close.bind(this)); 131 | 132 | this.inputEl.addEventListener('input', this.onInputChanged.bind(this)); 133 | this.inputEl.addEventListener('focus', this.onInputChanged.bind(this)); 134 | this.inputEl.addEventListener('blur', this.close.bind(this)); 135 | this.suggestEl.on( 136 | 'mousedown', 137 | '.suggestion-container', 138 | (event: MouseEvent) => { 139 | event.preventDefault(); 140 | } 141 | ); 142 | } 143 | 144 | onInputChanged(): void { 145 | const inputStr = this.inputEl.value; 146 | const suggestions = this.getSuggestions(inputStr); 147 | 148 | if (!suggestions) { 149 | this.close(); 150 | return; 151 | } 152 | 153 | if (suggestions.length > 0) { 154 | this.suggest.setSuggestions(suggestions); 155 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 156 | this.open((this.app).dom.appContainerEl, this.inputEl); 157 | } else { 158 | this.close(); 159 | } 160 | } 161 | 162 | open(container: HTMLElement, inputEl: HTMLElement): void { 163 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 164 | (this.app).keymap.pushScope(this.scope); 165 | 166 | container.appendChild(this.suggestEl); 167 | this.popper = createPopper(inputEl, this.suggestEl, { 168 | placement: 'bottom-start', 169 | modifiers: [ 170 | { 171 | name: 'sameWidth', 172 | enabled: true, 173 | fn: ({ state, instance }) => { 174 | // Note: positioning needs to be calculated twice - 175 | // first pass - positioning it according to the width of the popper 176 | // second pass - position it with the width bound to the reference element 177 | // we need to early exit to avoid an infinite loop 178 | const targetWidth = `${state.rects.reference.width}px`; 179 | if (state.styles.popper.width === targetWidth) { 180 | return; 181 | } 182 | state.styles.popper.width = targetWidth; 183 | instance.update(); 184 | }, 185 | phase: 'beforeWrite', 186 | requires: ['computeStyles'], 187 | }, 188 | ], 189 | }); 190 | } 191 | 192 | close(): void { 193 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 194 | (this.app).keymap.popScope(this.scope); 195 | 196 | this.suggest.setSuggestions([]); 197 | if (this.popper) this.popper.destroy(); 198 | this.suggestEl.detach(); 199 | } 200 | 201 | abstract getSuggestions(_inputStr: string): T[]; 202 | abstract renderSuggestion(_item: T, _el: HTMLElement): void; 203 | abstract selectSuggestion(_item: T): void; 204 | } 205 | -------------------------------------------------------------------------------- /src/uncover.ts: -------------------------------------------------------------------------------- 1 | import { App, Command, KeymapInfo } from "obsidian"; 2 | 3 | export class AppExtension extends App { 4 | commands: { 5 | commands: CommandMap 6 | } 7 | hotkeyManager: {"defaultKeys": {[key: string]: KeymapInfo[]}} 8 | } 9 | 10 | type CommandMap = { 11 | [key: string]: Command; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "lib": ["DOM", "ES5", "ES6", "ES7"], 13 | "experimentalDecorators": true, 14 | "emitDecoratorMetadata": true, 15 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 16 | 17 | "outDir": ".", 18 | /* Type Checking */ 19 | "strict": true /* Enable all strict type-checking options. */, 20 | "strictNullChecks": true /* When type checking, take into account `null` and `undefined`. */, 21 | "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */, 22 | "strictBindCallApply": true /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */, 23 | // "strictPropertyInitialization": true /* Check for class properties that are declared but not set in the constructor. */, 24 | "noImplicitThis": true /* Enable error reporting when `this` is given the type `any`. */, 25 | "useUnknownInCatchVariables": true /* Type catch clause variables as 'unknown' instead of 'any'. */, 26 | "alwaysStrict": true /* Ensure 'use strict' is always emitted. */, 27 | "noUnusedLocals": true /* Enable error reporting when a local variables aren't read. */, 28 | "noUnusedParameters": true /* Raise an error when a function parameter isn't read */, 29 | "exactOptionalPropertyTypes": true /* Interpret optional property types as written, rather than adding 'undefined'. */, 30 | "noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */, 31 | "noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */, 32 | "noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */, 33 | "noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */, 34 | "noPropertyAccessFromIndexSignature": true /* Enforces using indexed accessors for keys declared using an indexed type */, 35 | 36 | /* Completeness */ 37 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 38 | }, 39 | "include": ["src/*.ts"], 40 | "exclude": ["node_modules"] 41 | } 42 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.0.0": "0.12.0" 3 | } 4 | --------------------------------------------------------------------------------