├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── esbuild.config.mjs ├── manifest.json ├── package.json ├── src ├── CommandSuggest.ts ├── Modal.ts ├── Setting.ts ├── main.ts ├── types │ └── obsidian │ │ └── index.d.ts └── utils │ └── suggest.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': 'off', 25 | '@typescript-eslint/no-unused-vars': [ 26 | 'error', 27 | { argsIgnorePattern: '^_' }, 28 | ], 29 | }, 30 | settings: { 31 | react: { 32 | version: 'detect', 33 | }, 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /.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 | ## Command Palette-- 2 | 3 | Command palette without unwanted commands. 4 | Keep your command palette clean. 5 | 6 | ### How to use 7 | 8 | 1. Specify commands you want to hide in the setting tab. 9 | 2. Use `Command Palette--: Open command palette` instead of default `Command Palette: Open command palette`. 10 | 11 | 12 | -------------------------------------------------------------------------------- /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-command-palette-minus-plugin", 3 | "name": "Command Palette--", 4 | "version": "0.1.1", 5 | "minAppVersion": "0.12.0", 6 | "description": "Command palette without unwanted commands", 7 | "author": "qawatake", 8 | "authorUrl": "https://github.com/qawatake/obsidian-command-palette-minus-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/CommandSuggest.ts: -------------------------------------------------------------------------------- 1 | import CommandPaletteMinusPlugin, { GLOBAL_COMMAND_ID } from 'main'; 2 | import { App, Command, prepareFuzzySearch } from 'obsidian'; 3 | import { TextInputSuggest } from 'utils/suggest'; 4 | 5 | export class CommandSuggest extends TextInputSuggest { 6 | private plugin: CommandPaletteMinusPlugin; 7 | private _onSelected: ((cmd: Command) => void) | undefined; 8 | 9 | constructor( 10 | app: App, 11 | inputEl: HTMLInputElement | HTMLTextAreaElement, 12 | plugin: CommandPaletteMinusPlugin 13 | ) { 14 | super(app, inputEl); 15 | this.plugin = plugin; 16 | } 17 | 18 | getSuggestions(query: string): Command[] { 19 | const fuzzy = prepareFuzzySearch(query); 20 | const commands = Object.values(this.app.commands.commands); 21 | return ( 22 | commands 23 | // remove commands 24 | .filter( 25 | (cmd) => 26 | !Object.prototype.hasOwnProperty.call( 27 | this.plugin.settings?.removedCommands, 28 | cmd.id 29 | ) && cmd.id !== GLOBAL_COMMAND_ID 30 | ) 31 | // use score for sort 32 | .map((cmd) => { 33 | const result = fuzzy(cmd.name); 34 | return { score: result?.score, command: cmd }; 35 | }) 36 | .filter((result) => result.score !== undefined) 37 | // sort by score 38 | .sort((a, b) => { 39 | if (a.score !== undefined && b.score !== undefined) { 40 | return b.score - a.score; 41 | } else { 42 | console.log( 43 | '[ERROR in Command Palette--] failed to sort commands in suggestion.' 44 | ); 45 | return 0; 46 | } 47 | }) 48 | // abstract command 49 | .map((result) => result.command) 50 | ); 51 | } 52 | 53 | renderSuggestion(cmd: Command, el: HTMLElement): void { 54 | el.textContent = cmd.name; 55 | } 56 | 57 | selectSuggestion(cmd: Command): void { 58 | if (this._onSelected) { 59 | this._onSelected(cmd); 60 | } 61 | 62 | this.close(); 63 | } 64 | 65 | onSelected(cb: (cmd: Command) => void) { 66 | this._onSelected = cb; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Modal.ts: -------------------------------------------------------------------------------- 1 | import CommandPaletteMinusPlugin, { GLOBAL_COMMAND_ID } from 'main'; 2 | import { App, Command, FuzzySuggestModal } from 'obsidian'; 3 | 4 | export class CommandPaletteMinusModal extends FuzzySuggestModal { 5 | private plugin: CommandPaletteMinusPlugin; 6 | 7 | constructor(app: App, plugin: CommandPaletteMinusPlugin) { 8 | super(app); 9 | this.plugin = plugin; 10 | 11 | this.scope.register(['Ctrl'], 'p', () => { 12 | this.inputEl.dispatchEvent( 13 | new KeyboardEvent('keydown', { key: 'ArrowUp' }) 14 | ); 15 | }); 16 | this.scope.register(['Ctrl'], 'n', () => { 17 | this.inputEl.dispatchEvent( 18 | new KeyboardEvent('keydown', { key: 'ArrowDown' }) 19 | ); 20 | }); 21 | 22 | this.setInstructions([ 23 | { 24 | command: '↑↓', 25 | purpose: 'to navigate', 26 | }, 27 | { 28 | command: 'ctrl p/n', 29 | purpose: 'to navigate', 30 | }, 31 | { 32 | command: '↵', 33 | purpose: 'to use', 34 | }, 35 | { 36 | command: 'esc', 37 | purpose: 'to dismiss', 38 | }, 39 | ]); 40 | } 41 | 42 | getItems(): Command[] { 43 | return this.app.commands 44 | .listCommands() 45 | .filter( 46 | (cmd) => 47 | !Object.prototype.hasOwnProperty.call( 48 | this.plugin.settings?.removedCommands, 49 | cmd.id 50 | ) && cmd.id !== GLOBAL_COMMAND_ID 51 | ) 52 | .sort((cmd1, cmd2) => { 53 | const usedAt1 = this.plugin.settings?.usedCommands[cmd1.id]; 54 | const usedAt2 = this.plugin.settings?.usedCommands[cmd2.id]; 55 | if (usedAt1 === undefined && usedAt2 === undefined) return 0; 56 | if (usedAt1 !== undefined && usedAt2 !== undefined) { 57 | return usedAt2 - usedAt1; 58 | } 59 | return usedAt1 !== undefined ? -1 : 1; 60 | }); 61 | } 62 | 63 | getItemText(cmd: Command): string { 64 | return cmd.name; 65 | } 66 | 67 | onChooseItem(cmd: Command, _evt: MouseEvent | KeyboardEvent): void { 68 | this.app.commands.executeCommandById(cmd.id); 69 | if (!this.plugin.settings) return; 70 | this.plugin.settings.usedCommands[cmd.id] = Date.now(); 71 | this.plugin.saveSettings(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Setting.ts: -------------------------------------------------------------------------------- 1 | import { CommandSuggest } from 'CommandSuggest'; 2 | import CommandPaletteMinusPlugin from 'main'; 3 | import { App, Command, PluginSettingTab, Setting } from 'obsidian'; 4 | 5 | interface RemovedCommandMap { 6 | [commandId: string]: RegisteredAt; 7 | } 8 | type RegisteredAt = number; 9 | 10 | interface UsedCommandMap { 11 | [commandId: string]: UsedAt; 12 | } 13 | type UsedAt = number; 14 | 15 | export interface CommandPaletteMinusSettings { 16 | removedCommands: RemovedCommandMap; 17 | usedCommands: UsedCommandMap; 18 | } 19 | 20 | export const DEFAULT_SETTINGS: CommandPaletteMinusSettings = { 21 | removedCommands: {}, 22 | usedCommands: {}, 23 | }; 24 | 25 | export class CommandPaletteMinusSettingTab extends PluginSettingTab { 26 | private readonly plugin: CommandPaletteMinusPlugin; 27 | private inputEl: HTMLInputElement | undefined; 28 | 29 | constructor(app: App, plugin: CommandPaletteMinusPlugin) { 30 | super(app, plugin); 31 | this.plugin = plugin; 32 | } 33 | 34 | display() { 35 | const { containerEl } = this; 36 | containerEl.empty(); 37 | 38 | new Setting(containerEl) 39 | .setName('Remove command') 40 | .addSearch((component) => { 41 | this.inputEl = component.inputEl; 42 | new CommandSuggest( 43 | this.app, 44 | component.inputEl, 45 | this.plugin 46 | ).onSelected(async (cmd: Command) => { 47 | if (!this.plugin.settings) { 48 | return; 49 | } 50 | this.plugin.settings.removedCommands[cmd.id] = Date.now(); 51 | await this.plugin.saveSettings(); 52 | this.display(); 53 | this.focus(); 54 | }); 55 | }); 56 | 57 | Object.entries(this.plugin.settings?.removedCommands) 58 | // new ↓ old 59 | .sort((entry1, entry2) => { 60 | const timestamp1 = entry1[1], 61 | timestamp2 = entry2[1]; 62 | const bothNum = 63 | typeof timestamp1 === 'number' && 64 | typeof timestamp2 === 'number'; 65 | if (bothNum) { 66 | return timestamp2 - timestamp1; // new ↓ old 67 | } else { 68 | console.log( 69 | '[ERROR in Command Palette--] failed to sort commands in setting tab' 70 | ); 71 | return 0; 72 | } 73 | }) 74 | .forEach((entry) => { 75 | const id = entry[0]; 76 | if (typeof id !== 'string') { 77 | console.log( 78 | '[ERROR in Command Palette--]: failed to display commands: wrong type' 79 | ); 80 | return; 81 | } 82 | const cmd = this.app.commands.findCommand(id); 83 | if (!cmd) { 84 | return; 85 | } 86 | new Setting(containerEl) 87 | .setName(cmd.name) 88 | .addExtraButton((component) => { 89 | component.setIcon('cross').onClick(async () => { 90 | delete this.plugin.settings?.removedCommands[ 91 | cmd.id 92 | ]; 93 | await this.plugin.saveSettings(); 94 | this.display(); 95 | }); 96 | }); 97 | }); 98 | } 99 | 100 | private focus() { 101 | this.inputEl?.focus(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { CommandPaletteMinusModal } from 'Modal'; 2 | import { Plugin } from 'obsidian'; 3 | import { 4 | CommandPaletteMinusSettings, 5 | CommandPaletteMinusSettingTab, 6 | DEFAULT_SETTINGS, 7 | } from 'Setting'; 8 | 9 | const LOCAL_COMMAND_ID = 'open'; 10 | const PLUGIN_ID = 'obsidian-command-palette-minus-plugin'; 11 | export const GLOBAL_COMMAND_ID = `${PLUGIN_ID}:${LOCAL_COMMAND_ID}`; 12 | 13 | export default class CommandPaletteMinusPlugin extends Plugin { 14 | settings: CommandPaletteMinusSettings | undefined; 15 | 16 | override async onload() { 17 | await this.loadSettings(); 18 | 19 | this.addCommand({ 20 | id: LOCAL_COMMAND_ID, 21 | name: 'Open command palette', 22 | callback: () => { 23 | new CommandPaletteMinusModal(this.app, this).open(); 24 | }, 25 | }); 26 | 27 | this.addSettingTab(new CommandPaletteMinusSettingTab(this.app, this)); 28 | } 29 | 30 | // override onunload() {} 31 | 32 | async loadSettings() { 33 | this.settings = Object.assign( 34 | {}, 35 | DEFAULT_SETTINGS, 36 | await this.loadData() 37 | ); 38 | } 39 | 40 | async saveSettings() { 41 | await this.saveData(this.settings); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/types/obsidian/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from 'obsidian'; 2 | 3 | declare module 'obsidian' { 4 | interface App { 5 | commands: CommandManager; 6 | } 7 | 8 | interface CommandManager { 9 | listCommands(): Command[]; // list only available commands 10 | commands: CommandMap; // list all commands 11 | executeCommandById(id: string): void; 12 | findCommand(id: string): Command | undefined | null; 13 | } 14 | 15 | interface CommandMap { 16 | [commandId: string]: Command; 17 | } 18 | 19 | interface FuzzySuggestModal { 20 | chooser: Chooser; 21 | } 22 | 23 | interface Chooser { 24 | selectedItem: number; 25 | setSelectedItem(item: number): void; 26 | useSelectedItem(ev: Partial): void; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/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: HTMLElement[] = []; 14 | private selectedItem = 0; 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 | return false; 42 | }); 43 | scope.register(['Ctrl'], 'p', (event) => { 44 | if (!event.isComposing) { 45 | this.setSelectedItem(this.selectedItem - 1, true); 46 | } 47 | return false; 48 | }); 49 | 50 | scope.register([], 'ArrowDown', (event) => { 51 | if (!event.isComposing) { 52 | this.setSelectedItem(this.selectedItem + 1, true); 53 | return false; 54 | } 55 | return false; 56 | }); 57 | scope.register(['Ctrl'], 'n', (event) => { 58 | if (!event.isComposing) { 59 | this.setSelectedItem(this.selectedItem + 1, true); 60 | return false; 61 | } 62 | return false; 63 | }); 64 | 65 | scope.register([], 'Enter', (event) => { 66 | if (!event.isComposing) { 67 | this.useSelectedItem(event); 68 | return false; 69 | } 70 | return false; 71 | }); 72 | } 73 | 74 | onSuggestionClick(event: MouseEvent, el: HTMLElement): void { 75 | event.preventDefault(); 76 | 77 | const item = this.suggestions.indexOf(el); 78 | this.setSelectedItem(item, false); 79 | this.useSelectedItem(event); 80 | } 81 | 82 | onSuggestionMouseover(_event: MouseEvent, el: HTMLElement): void { 83 | const item = this.suggestions.indexOf(el); 84 | this.setSelectedItem(item, false); 85 | } 86 | 87 | setSuggestions(values: T[]) { 88 | this.containerEl.empty(); 89 | const suggestionEls: HTMLDivElement[] = []; 90 | 91 | values.forEach((value) => { 92 | const suggestionEl = this.containerEl.createDiv('suggestion-item'); 93 | this.owner.renderSuggestion(value, suggestionEl); 94 | suggestionEls.push(suggestionEl); 95 | }); 96 | 97 | this.values = values; 98 | this.suggestions = suggestionEls; 99 | this.setSelectedItem(0, false); 100 | } 101 | 102 | useSelectedItem(event: MouseEvent | KeyboardEvent) { 103 | const currentValue = this.values[this.selectedItem]; 104 | if (currentValue) { 105 | this.owner.selectSuggestion(currentValue, event); 106 | } 107 | } 108 | 109 | setSelectedItem(selectedIndex: number, scrollIntoView: boolean) { 110 | const normalizedIndex = wrapAround( 111 | selectedIndex, 112 | this.suggestions.length 113 | ); 114 | const prevSelectedSuggestion = this.suggestions[this.selectedItem]; 115 | const selectedSuggestion = this.suggestions[normalizedIndex]; 116 | 117 | prevSelectedSuggestion?.removeClass('is-selected'); 118 | selectedSuggestion?.addClass('is-selected'); 119 | 120 | this.selectedItem = normalizedIndex; 121 | 122 | if (scrollIntoView) { 123 | selectedSuggestion?.scrollIntoView(false); 124 | } 125 | } 126 | } 127 | 128 | export abstract class TextInputSuggest implements ISuggestOwner { 129 | protected app: App; 130 | protected inputEl: HTMLInputElement | HTMLTextAreaElement; 131 | 132 | private popper: PopperInstance | undefined; 133 | private scope: Scope; 134 | private suggestEl: HTMLElement; 135 | private suggest: Suggest; 136 | 137 | constructor(app: App, inputEl: HTMLInputElement | HTMLTextAreaElement) { 138 | this.app = app; 139 | this.inputEl = inputEl; 140 | this.scope = new Scope(); 141 | 142 | this.suggestEl = createDiv('suggestion-container'); 143 | const suggestion = this.suggestEl.createDiv('suggestion'); 144 | this.suggest = new Suggest(this, suggestion, this.scope); 145 | 146 | this.scope.register([], 'Escape', this.close.bind(this)); 147 | 148 | this.inputEl.addEventListener('input', this.onInputChanged.bind(this)); 149 | this.inputEl.addEventListener('focus', this.onInputChanged.bind(this)); 150 | this.inputEl.addEventListener('blur', this.close.bind(this)); 151 | this.suggestEl.on( 152 | 'mousedown', 153 | '.suggestion-container', 154 | (event: MouseEvent) => { 155 | event.preventDefault(); 156 | } 157 | ); 158 | } 159 | 160 | onInputChanged(): void { 161 | const inputStr = this.inputEl.value; 162 | const suggestions = this.getSuggestions(inputStr); 163 | 164 | if (!suggestions) { 165 | this.close(); 166 | return; 167 | } 168 | 169 | if (suggestions.length > 0) { 170 | this.suggest.setSuggestions(suggestions); 171 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 172 | this.open((this.app).dom.appContainerEl, this.inputEl); 173 | } else { 174 | this.close(); 175 | } 176 | } 177 | 178 | open(container: HTMLElement, inputEl: HTMLElement): void { 179 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 180 | (this.app).keymap.pushScope(this.scope); 181 | 182 | container.appendChild(this.suggestEl); 183 | this.popper = createPopper(inputEl, this.suggestEl, { 184 | placement: 'bottom-start', 185 | modifiers: [ 186 | { 187 | name: 'sameWidth', 188 | enabled: true, 189 | fn: ({ state, instance }) => { 190 | // Note: positioning needs to be calculated twice - 191 | // first pass - positioning it according to the width of the popper 192 | // second pass - position it with the width bound to the reference element 193 | // we need to early exit to avoid an infinite loop 194 | const targetWidth = `${state.rects.reference.width}px`; 195 | const p = state.styles['popper']; 196 | if (p === undefined) { 197 | return; 198 | } 199 | if (p.width === targetWidth) { 200 | return; 201 | } 202 | p.width = targetWidth; 203 | instance.update(); 204 | }, 205 | phase: 'beforeWrite', 206 | requires: ['computeStyles'], 207 | }, 208 | ], 209 | }); 210 | } 211 | 212 | close(): void { 213 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 214 | (this.app).keymap.popScope(this.scope); 215 | 216 | this.suggest.setSuggestions([]); 217 | if (this.popper) this.popper.destroy(); 218 | this.suggestEl.detach(); 219 | } 220 | 221 | abstract getSuggestions(_inputStr: string): T[]; 222 | abstract renderSuggestion(_item: T, _el: HTMLElement): void; 223 | abstract selectSuggestion(_item: T): void; 224 | } 225 | -------------------------------------------------------------------------------- /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 | "1.0.1": "0.9.12", 3 | "1.0.0": "0.9.7" 4 | } 5 | --------------------------------------------------------------------------------