scaffolding
This commit is contained in:
parent
2ed775bdd4
commit
ddcc767573
13 changed files with 3717 additions and 151 deletions
|
|
@ -15,7 +15,7 @@ esbuild.build({
|
||||||
banner: {
|
banner: {
|
||||||
js: banner,
|
js: banner,
|
||||||
},
|
},
|
||||||
entryPoints: ['main.ts'],
|
entryPoints: ['src/main.ts'],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
external: [
|
external: [
|
||||||
'obsidian',
|
'obsidian',
|
||||||
|
|
|
||||||
137
main.ts
137
main.ts
|
|
@ -1,137 +0,0 @@
|
||||||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
|
|
||||||
|
|
||||||
// Remember to rename these classes and interfaces!
|
|
||||||
|
|
||||||
interface MyPluginSettings {
|
|
||||||
mySetting: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: MyPluginSettings = {
|
|
||||||
mySetting: 'default'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class MyPlugin extends Plugin {
|
|
||||||
settings: MyPluginSettings;
|
|
||||||
|
|
||||||
async onload() {
|
|
||||||
await this.loadSettings();
|
|
||||||
|
|
||||||
// This creates an icon in the left ribbon.
|
|
||||||
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
|
|
||||||
// Called when the user clicks the icon.
|
|
||||||
new Notice('This is a notice!');
|
|
||||||
});
|
|
||||||
// Perform additional things with the ribbon
|
|
||||||
ribbonIconEl.addClass('my-plugin-ribbon-class');
|
|
||||||
|
|
||||||
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
|
|
||||||
const statusBarItemEl = this.addStatusBarItem();
|
|
||||||
statusBarItemEl.setText('Status Bar Text');
|
|
||||||
|
|
||||||
// This adds a simple command that can be triggered anywhere
|
|
||||||
this.addCommand({
|
|
||||||
id: 'open-sample-modal-simple',
|
|
||||||
name: 'Open sample modal (simple)',
|
|
||||||
callback: () => {
|
|
||||||
new SampleModal(this.app).open();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// This adds an editor command that can perform some operation on the current editor instance
|
|
||||||
this.addCommand({
|
|
||||||
id: 'sample-editor-command',
|
|
||||||
name: 'Sample editor command',
|
|
||||||
editorCallback: (editor: Editor, view: MarkdownView) => {
|
|
||||||
console.log(editor.getSelection());
|
|
||||||
editor.replaceSelection('Sample Editor Command');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// This adds a complex command that can check whether the current state of the app allows execution of the command
|
|
||||||
this.addCommand({
|
|
||||||
id: 'open-sample-modal-complex',
|
|
||||||
name: 'Open sample modal (complex)',
|
|
||||||
checkCallback: (checking: boolean) => {
|
|
||||||
// Conditions to check
|
|
||||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
||||||
if (markdownView) {
|
|
||||||
// If checking is true, we're simply "checking" if the command can be run.
|
|
||||||
// If checking is false, then we want to actually perform the operation.
|
|
||||||
if (!checking) {
|
|
||||||
new SampleModal(this.app).open();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This command will only show up in Command Palette when the check function returns true
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
|
||||||
this.addSettingTab(new SampleSettingTab(this.app, this));
|
|
||||||
|
|
||||||
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
|
|
||||||
// Using this function will automatically remove the event listener when this plugin is disabled.
|
|
||||||
this.registerDomEvent(document, 'click', (evt: MouseEvent) => {
|
|
||||||
console.log('click', evt);
|
|
||||||
});
|
|
||||||
|
|
||||||
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
|
|
||||||
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
onunload() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadSettings() {
|
|
||||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveSettings() {
|
|
||||||
await this.saveData(this.settings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SampleModal extends Modal {
|
|
||||||
constructor(app: App) {
|
|
||||||
super(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
onOpen() {
|
|
||||||
const {contentEl} = this;
|
|
||||||
contentEl.setText('Woah!');
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose() {
|
|
||||||
const {contentEl} = this;
|
|
||||||
contentEl.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SampleSettingTab extends PluginSettingTab {
|
|
||||||
plugin: MyPlugin;
|
|
||||||
|
|
||||||
constructor(app: App, plugin: MyPlugin) {
|
|
||||||
super(app, plugin);
|
|
||||||
this.plugin = plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
display(): void {
|
|
||||||
const {containerEl} = this;
|
|
||||||
|
|
||||||
containerEl.empty();
|
|
||||||
|
|
||||||
containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'});
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Setting #1')
|
|
||||||
.setDesc('It\'s a secret')
|
|
||||||
.addText(text => text
|
|
||||||
.setPlaceholder('Enter your secret')
|
|
||||||
.setValue(this.plugin.settings.mySetting)
|
|
||||||
.onChange(async (value) => {
|
|
||||||
console.log('Secret: ' + value);
|
|
||||||
this.plugin.settings.mySetting = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"id": "obsidian-sample-plugin",
|
"id": "obsidian-media-db-plugin",
|
||||||
"name": "Sample Plugin",
|
"name": "Media DB Plugin",
|
||||||
"version": "1.0.1",
|
"version": "0.1.0",
|
||||||
"minAppVersion": "0.12.0",
|
"minAppVersion": "0.12.0",
|
||||||
"description": "This is a sample plugin for Obsidian. This plugin demonstrates some of the capabilities of the Obsidian API.",
|
"description": "Coming soon TM",
|
||||||
"author": "Obsidian",
|
"author": "Moritz Jung",
|
||||||
"authorUrl": "https://obsidian.md",
|
"authorUrl": "https://mprojectscode.github.io/",
|
||||||
"isDesktopOnly": false
|
"isDesktopOnly": false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3292
package-lock.json
generated
Normal file
3292
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "obsidian-sample-plugin",
|
"name": "obsidian-media-db-plugin",
|
||||||
"version": "1.0.1",
|
"version": "0.1.0",
|
||||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
"description": "Coming soon TM",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node esbuild.config.mjs",
|
"dev": "node esbuild.config.mjs",
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@popperjs/core": "^2.11.5",
|
||||||
"@types/node": "^16.11.6",
|
"@types/node": "^16.11.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.2.0",
|
"@typescript-eslint/eslint-plugin": "^5.2.0",
|
||||||
"@typescript-eslint/parser": "^5.2.0",
|
"@typescript-eslint/parser": "^5.2.0",
|
||||||
|
|
|
||||||
6
src/api/APIResult.ts
Normal file
6
src/api/APIResult.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export interface APIResult {
|
||||||
|
title: string,
|
||||||
|
type: string,
|
||||||
|
description?: string,
|
||||||
|
data: any,
|
||||||
|
}
|
||||||
50
src/main.ts
Normal file
50
src/main.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import {Plugin} from 'obsidian';
|
||||||
|
import {DEFAULT_SETTINGS, MediaDbPluginSettings, MediaDbSettingTab} from './settings/settings';
|
||||||
|
import {MediaDbSearchModal} from './modals/MediaDbSearchModal';
|
||||||
|
|
||||||
|
export default class MediaDbPlugin extends Plugin {
|
||||||
|
settings: MediaDbPluginSettings;
|
||||||
|
|
||||||
|
async onload() {
|
||||||
|
await this.loadSettings();
|
||||||
|
|
||||||
|
// add icon to the left ribbon
|
||||||
|
const ribbonIconEl = this.addRibbonIcon('book', 'Add new Media DB entry', (evt: MouseEvent) =>
|
||||||
|
this.createMediaDbNote(),
|
||||||
|
);
|
||||||
|
ribbonIconEl.addClass('obsidian-media-db-plugin-ribbon-class');
|
||||||
|
|
||||||
|
// register command to open search modal
|
||||||
|
this.addCommand({
|
||||||
|
id: 'open-media-db-search-modal',
|
||||||
|
name: 'Add new Media DB entry',
|
||||||
|
callback: () => this.createMediaDbNote(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// register the settings tab
|
||||||
|
this.addSettingTab(new MediaDbSettingTab(this.app, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
async createMediaDbNote(): Promise<void> {
|
||||||
|
const data = await this.openMediaDbSearchModal();
|
||||||
|
console.log('Create new note or something...');
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async openMediaDbSearchModal(): Promise<any> {
|
||||||
|
return new Promise(((resolve, reject) => {
|
||||||
|
new MediaDbSearchModal(this.app, (err, result) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(result);
|
||||||
|
}).open();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSettings() {
|
||||||
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings() {
|
||||||
|
await this.saveData(this.settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/modals/MediaDbSearchModal.ts
Normal file
96
src/modals/MediaDbSearchModal.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import {App, ButtonComponent, SuggestModal} from 'obsidian';
|
||||||
|
import {APIResult} from '../api/APIResult';
|
||||||
|
import {sleep} from '../utils/utils';
|
||||||
|
|
||||||
|
export class MediaDbSearchModal extends SuggestModal<APIResult> {
|
||||||
|
query: string;
|
||||||
|
hasChanged: boolean;
|
||||||
|
isBusy: boolean;
|
||||||
|
okBtn: ButtonComponent;
|
||||||
|
onChoose: (err: Error, result?: APIResult) => void;
|
||||||
|
|
||||||
|
constructor(app: App, onChoose?: (err: Error, result?: APIResult) => void) {
|
||||||
|
super(app);
|
||||||
|
|
||||||
|
this.onChoose = onChoose;
|
||||||
|
this.isBusy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(force: boolean = false): Promise<APIResult[]> {
|
||||||
|
if (!this.query) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isBusy || force) {
|
||||||
|
this.isBusy = true;
|
||||||
|
const thisQuery: string = this.query;
|
||||||
|
|
||||||
|
console.log('query started with ' + thisQuery)
|
||||||
|
await sleep(1000); // TODO: replace with real api call
|
||||||
|
|
||||||
|
if (this.query === thisQuery) {
|
||||||
|
this.isBusy = false;
|
||||||
|
return [ // TODO: replace with real api result
|
||||||
|
{title: thisQuery, type: 'movie', data: null} as APIResult,
|
||||||
|
{title: 'test2', type: 'series', data: {episodes: 24}} as APIResult,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return await this.search(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSuggestions(query: string): Promise<APIResult[]> {
|
||||||
|
this.query = query;
|
||||||
|
|
||||||
|
return await this.search();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSuggestion(item: APIResult, el: HTMLElement): void {
|
||||||
|
el.createEl('div', {text: item.title});
|
||||||
|
el.createEl('small', {text: item.type});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChooseSuggestion(item: APIResult, evt: MouseEvent | KeyboardEvent): void {
|
||||||
|
this.onChoose(null, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
onOpen() {
|
||||||
|
const { contentEl } = this;
|
||||||
|
|
||||||
|
contentEl.createEl('h2', { text: 'Search Media DB' });
|
||||||
|
|
||||||
|
const placeholder = 'Search by title';
|
||||||
|
const textComponent = new TextComponent(contentEl);
|
||||||
|
|
||||||
|
textComponent.setPlaceholder(placeholder);
|
||||||
|
textComponent.onChange(value => (this.query = value));
|
||||||
|
|
||||||
|
textComponent.inputEl.addEventListener('keydown', this.submitCallback.bind(this));
|
||||||
|
textComponent.inputEl.style.width = '100%';
|
||||||
|
|
||||||
|
contentEl.appendChild(textComponent.inputEl);
|
||||||
|
textComponent.inputEl.focus();
|
||||||
|
|
||||||
|
const resultsComponent = new
|
||||||
|
|
||||||
|
new Setting(contentEl)
|
||||||
|
.addButton(btn => btn.setButtonText('Cancel').onClick(() => this.close()))
|
||||||
|
.addButton(btn => {
|
||||||
|
return (this.okBtn = btn
|
||||||
|
.setButtonText('Ok')
|
||||||
|
.setCta()
|
||||||
|
.onClick(() => {
|
||||||
|
this.search();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
const { contentEl } = this;
|
||||||
|
contentEl.empty();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
41
src/settings/settings.ts
Normal file
41
src/settings/settings.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import {App, PluginSettingTab, Setting} from 'obsidian';
|
||||||
|
|
||||||
|
import MediaDbPlugin from '../main';
|
||||||
|
import {FolderSuggest} from './suggesters/FolderSuggester';
|
||||||
|
|
||||||
|
|
||||||
|
export interface MediaDbPluginSettings {
|
||||||
|
folder: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SETTINGS: MediaDbPluginSettings = {
|
||||||
|
folder: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MediaDbSettingTab extends PluginSettingTab {
|
||||||
|
plugin: MediaDbPlugin;
|
||||||
|
|
||||||
|
constructor(app: App, plugin: MediaDbPlugin) {
|
||||||
|
super(app, plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
const {containerEl} = this;
|
||||||
|
|
||||||
|
containerEl.createEl('h2', {text: 'Media DB Plugin Settings'});
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName('New file location')
|
||||||
|
.setDesc('New book notes will be placed here.')
|
||||||
|
.addSearch(cb => {
|
||||||
|
new FolderSuggest(this.app, cb.inputEl);
|
||||||
|
cb.setPlaceholder('Example: folder1/folder2')
|
||||||
|
.setValue(this.plugin.settings.folder)
|
||||||
|
.onChange(data => {
|
||||||
|
this.plugin.settings.folder = data;
|
||||||
|
this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
33
src/settings/suggesters/FolderSuggester.ts
Normal file
33
src/settings/suggesters/FolderSuggester.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
|
||||||
|
|
||||||
|
import {TAbstractFile, TFolder} from 'obsidian';
|
||||||
|
import {TextInputSuggest} from './suggest';
|
||||||
|
|
||||||
|
export class FolderSuggest extends TextInputSuggest<TFolder> {
|
||||||
|
getSuggestions(inputStr: string): TFolder[] {
|
||||||
|
const abstractFiles = this.app.vault.getAllLoadedFiles();
|
||||||
|
const folders: TFolder[] = [];
|
||||||
|
const lowerCaseInputStr = inputStr.toLowerCase();
|
||||||
|
|
||||||
|
abstractFiles.forEach((folder: TAbstractFile) => {
|
||||||
|
if (
|
||||||
|
folder instanceof TFolder &&
|
||||||
|
folder.path.toLowerCase().contains(lowerCaseInputStr)
|
||||||
|
) {
|
||||||
|
folders.push(folder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return folders;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSuggestion(file: TFolder, el: HTMLElement): void {
|
||||||
|
el.setText(file.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectSuggestion(file: TFolder): void {
|
||||||
|
this.inputEl.value = file.path;
|
||||||
|
this.inputEl.trigger('input');
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
181
src/settings/suggesters/suggest.ts
Normal file
181
src/settings/suggesters/suggest.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
|
||||||
|
|
||||||
|
import {App, ISuggestOwner, Scope} from 'obsidian';
|
||||||
|
import {createPopper, Instance as PopperInstance} from '@popperjs/core';
|
||||||
|
import {wrapAround} from 'src/utils/utils';
|
||||||
|
|
||||||
|
export class Suggest<T> {
|
||||||
|
private owner: ISuggestOwner<T>;
|
||||||
|
private values: T[];
|
||||||
|
private suggestions: HTMLDivElement[];
|
||||||
|
private selectedItem: number;
|
||||||
|
private containerEl: HTMLElement;
|
||||||
|
|
||||||
|
constructor(owner: ISuggestOwner<T>, containerEl: HTMLElement, scope: Scope) {
|
||||||
|
this.owner = owner;
|
||||||
|
this.containerEl = containerEl;
|
||||||
|
|
||||||
|
containerEl.on('click', '.suggestion-item', this.onSuggestionClick.bind(this));
|
||||||
|
containerEl.on(
|
||||||
|
'mousemove',
|
||||||
|
'.suggestion-item',
|
||||||
|
this.onSuggestionMouseover.bind(this),
|
||||||
|
);
|
||||||
|
|
||||||
|
scope.register([], 'ArrowUp', (event) => {
|
||||||
|
if (!event.isComposing) {
|
||||||
|
this.setSelectedItem(this.selectedItem - 1, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.register([], 'ArrowDown', (event) => {
|
||||||
|
if (!event.isComposing) {
|
||||||
|
this.setSelectedItem(this.selectedItem + 1, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scope.register([], 'Enter', (event) => {
|
||||||
|
if (!event.isComposing) {
|
||||||
|
this.useSelectedItem(event);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const item = this.suggestions.indexOf(el);
|
||||||
|
this.setSelectedItem(item, false);
|
||||||
|
this.useSelectedItem(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void {
|
||||||
|
const item = this.suggestions.indexOf(el);
|
||||||
|
this.setSelectedItem(item, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuggestions(values: T[]) {
|
||||||
|
this.containerEl.empty();
|
||||||
|
const suggestionEls: HTMLDivElement[] = [];
|
||||||
|
|
||||||
|
values.forEach((value) => {
|
||||||
|
const suggestionEl = this.containerEl.createDiv('suggestion-item');
|
||||||
|
this.owner.renderSuggestion(value, suggestionEl);
|
||||||
|
suggestionEls.push(suggestionEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.values = values;
|
||||||
|
this.suggestions = suggestionEls;
|
||||||
|
this.setSelectedItem(0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
useSelectedItem(event: MouseEvent | KeyboardEvent) {
|
||||||
|
const currentValue = this.values[this.selectedItem];
|
||||||
|
if (currentValue) {
|
||||||
|
this.owner.selectSuggestion(currentValue, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedItem(selectedIndex: number, scrollIntoView: boolean) {
|
||||||
|
const normalizedIndex = wrapAround(selectedIndex, this.suggestions.length);
|
||||||
|
const prevSelectedSuggestion = this.suggestions[this.selectedItem];
|
||||||
|
const selectedSuggestion = this.suggestions[normalizedIndex];
|
||||||
|
|
||||||
|
prevSelectedSuggestion?.removeClass('is-selected');
|
||||||
|
selectedSuggestion?.addClass('is-selected');
|
||||||
|
|
||||||
|
this.selectedItem = normalizedIndex;
|
||||||
|
|
||||||
|
if (scrollIntoView) {
|
||||||
|
selectedSuggestion.scrollIntoView(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class TextInputSuggest<T> implements ISuggestOwner<T> {
|
||||||
|
protected app: App;
|
||||||
|
protected inputEl: HTMLInputElement;
|
||||||
|
|
||||||
|
private popper: PopperInstance;
|
||||||
|
private scope: Scope;
|
||||||
|
private suggestEl: HTMLElement;
|
||||||
|
private suggest: Suggest<T>;
|
||||||
|
|
||||||
|
constructor(app: App, inputEl: HTMLInputElement) {
|
||||||
|
this.app = app;
|
||||||
|
this.inputEl = inputEl;
|
||||||
|
this.scope = new Scope();
|
||||||
|
|
||||||
|
this.suggestEl = createDiv('suggestion-container');
|
||||||
|
const suggestion = this.suggestEl.createDiv('suggestion');
|
||||||
|
this.suggest = new Suggest(this, suggestion, this.scope);
|
||||||
|
|
||||||
|
this.scope.register([], 'Escape', this.close.bind(this));
|
||||||
|
|
||||||
|
this.inputEl.addEventListener('input', this.onInputChanged.bind(this));
|
||||||
|
this.inputEl.addEventListener('focus', this.onInputChanged.bind(this));
|
||||||
|
this.inputEl.addEventListener('blur', this.close.bind(this));
|
||||||
|
this.suggestEl.on('mousedown', '.suggestion-container', (event: MouseEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onInputChanged(): void {
|
||||||
|
const inputStr = this.inputEl.value;
|
||||||
|
const suggestions = this.getSuggestions(inputStr);
|
||||||
|
|
||||||
|
if (suggestions.length > 0) {
|
||||||
|
this.suggest.setSuggestions(suggestions);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
this.open((<any>this.app).dom.appContainerEl, this.inputEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open(container: HTMLElement, inputEl: HTMLElement): void {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(<any>this.app).keymap.pushScope(this.scope);
|
||||||
|
|
||||||
|
container.appendChild(this.suggestEl);
|
||||||
|
this.popper = createPopper(inputEl, this.suggestEl, {
|
||||||
|
placement: 'bottom-start',
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: 'sameWidth',
|
||||||
|
enabled: true,
|
||||||
|
fn: ({state, instance}) => {
|
||||||
|
// Note: positioning needs to be calculated twice -
|
||||||
|
// first pass - positioning it according to the width of the popper
|
||||||
|
// second pass - position it with the width bound to the reference element
|
||||||
|
// we need to early exit to avoid an infinite loop
|
||||||
|
const targetWidth = `${state.rects.reference.width}px`;
|
||||||
|
if (state.styles.popper.width === targetWidth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.styles.popper.width = targetWidth;
|
||||||
|
instance.update();
|
||||||
|
},
|
||||||
|
phase: 'beforeWrite',
|
||||||
|
requires: ['computeStyles'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(<any>this.app).keymap.popScope(this.scope);
|
||||||
|
|
||||||
|
this.suggest.setSuggestions([]);
|
||||||
|
this.popper.destroy();
|
||||||
|
this.suggestEl.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getSuggestions(inputStr: string): T[];
|
||||||
|
|
||||||
|
abstract renderSuggestion(item: T, el: HTMLElement): void;
|
||||||
|
|
||||||
|
abstract selectSuggestion(item: T): void;
|
||||||
|
}
|
||||||
7
src/utils/utils.ts
Normal file
7
src/utils/utils.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const wrapAround = (value: number, size: number): number => {
|
||||||
|
return ((value % size) + size) % size;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sleep = (ms: number) => {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
};
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
/* Sets all the text color to red! */
|
|
||||||
body {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue