initial
This commit is contained in:
parent
866d3b8f0d
commit
2eef8f00b1
|
@ -2,9 +2,9 @@
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
indent_style = tab
|
|
||||||
indent_size = 4
|
|
||||||
tab_width = 4
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"arrowParens": "always",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"endOfLine": "auto",
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"printWidth": 80,
|
||||||
|
"proseWrap": "never",
|
||||||
|
"quoteProps": "preserve",
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Notice } from 'obsidian';
|
||||||
|
|
||||||
|
export class TemplaterError extends Error {
|
||||||
|
constructor(msg: string, public console_msg?: string) {
|
||||||
|
super(msg);
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function log_error(e: Error | TemplaterError): void {
|
||||||
|
const notice = new Notice('', 8000);
|
||||||
|
if (e instanceof TemplaterError && e.console_msg) {
|
||||||
|
// TODO: Find a better way for this
|
||||||
|
// @ts-ignore
|
||||||
|
notice.noticeEl.innerHTML = `<b>Templater Error</b>:<br/>${e.message}<br/>Check console for more information`;
|
||||||
|
console.error(`Templater Error:`, e.message, '\n', e.console_msg);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
notice.noticeEl.innerHTML = `<b>Templater Error</b>:<br/>${e.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function errorWrapperSync<T>(fn: () => T, msg: string): T {
|
||||||
|
try {
|
||||||
|
return fn();
|
||||||
|
} catch (e) {
|
||||||
|
log_error(new TemplaterError(msg, e.message));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FileSuggestMode {
|
||||||
|
TemplateFiles,
|
||||||
|
ScriptFiles,
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
import {
|
||||||
|
App,
|
||||||
|
normalizePath,
|
||||||
|
TAbstractFile,
|
||||||
|
TFile,
|
||||||
|
TFolder,
|
||||||
|
Vault,
|
||||||
|
} from 'obsidian';
|
||||||
|
import { TemplaterError } from '../errors/errors';
|
||||||
|
|
||||||
|
export function resolveFolder(app: App, folder_str: string): TFolder {
|
||||||
|
folder_str = normalizePath(folder_str);
|
||||||
|
|
||||||
|
const folder = app.vault.getAbstractFileByPath(folder_str);
|
||||||
|
if (!folder) {
|
||||||
|
throw new TemplaterError(`Folder "${folder_str}" doesn't exist`);
|
||||||
|
}
|
||||||
|
if (!(folder instanceof TFolder)) {
|
||||||
|
throw new TemplaterError(`${folder_str} is a file, not a folder`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFilesFromTheFolder(
|
||||||
|
app: App,
|
||||||
|
folder_str: string,
|
||||||
|
): Array<TFile> {
|
||||||
|
const folder = resolveFolder(app, folder_str);
|
||||||
|
|
||||||
|
const files: Array<TFile> = [];
|
||||||
|
Vault.recurseChildren(folder, (file: TAbstractFile) => {
|
||||||
|
if (file instanceof TFile) {
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
files.sort((a, b) => {
|
||||||
|
return a.basename.localeCompare(b.basename);
|
||||||
|
});
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
335
main.ts
335
main.ts
|
@ -1,137 +1,236 @@
|
||||||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian';
|
import {
|
||||||
|
App,
|
||||||
// Remember to rename these classes and interfaces!
|
FileManager,
|
||||||
|
Plugin,
|
||||||
|
PluginSettingTab,
|
||||||
|
Setting,
|
||||||
|
TAbstractFile,
|
||||||
|
TFile,
|
||||||
|
} from 'obsidian';
|
||||||
|
import { FolderSuggest } from './suggest/folderSuggest';
|
||||||
|
|
||||||
interface MyPluginSettings {
|
interface MyPluginSettings {
|
||||||
mySetting: string;
|
folderName: string;
|
||||||
|
fileNames: TFile[];
|
||||||
|
existingSymbol: string;
|
||||||
|
replacePattern: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: MyPluginSettings = {
|
const DEFAULT_SETTINGS: MyPluginSettings = {
|
||||||
mySetting: 'default'
|
folderName: '',
|
||||||
}
|
fileNames: [],
|
||||||
|
existingSymbol: '',
|
||||||
|
replacePattern: '',
|
||||||
|
};
|
||||||
|
|
||||||
export default class MyPlugin extends Plugin {
|
export default class MyPlugin extends Plugin {
|
||||||
settings: MyPluginSettings;
|
settings: MyPluginSettings;
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
|
this.addSettingTab(new BulkRenameSettingsTab(this.app, this));
|
||||||
// This creates an icon in the left ribbon.
|
}
|
||||||
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => {
|
async loadSettings() {
|
||||||
// Called when the user clicks the icon.
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||||
new Notice('This is a notice!');
|
}
|
||||||
});
|
async saveSettings() {
|
||||||
// Perform additional things with the ribbon
|
await this.saveData(this.settings);
|
||||||
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 {
|
type State = {
|
||||||
constructor(app: App) {
|
previewScroll: number;
|
||||||
super(app);
|
filesScroll: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
onOpen() {
|
class BulkRenameSettingsTab extends PluginSettingTab {
|
||||||
const {contentEl} = this;
|
plugin: MyPlugin;
|
||||||
contentEl.setText('Woah!');
|
state: State;
|
||||||
}
|
|
||||||
|
|
||||||
onClose() {
|
constructor(app: App, plugin: MyPlugin) {
|
||||||
const {contentEl} = this;
|
super(app, plugin);
|
||||||
contentEl.empty();
|
this.state = {
|
||||||
}
|
previewScroll: 0,
|
||||||
|
filesScroll: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
display() {
|
||||||
|
const { containerEl } = this;
|
||||||
|
containerEl.empty();
|
||||||
|
containerEl.createEl('h2', { text: 'General Settings' });
|
||||||
|
this.renderFileLocation();
|
||||||
|
this.renderReplaceSymbol();
|
||||||
|
this.renderFilesAndPreview();
|
||||||
|
this.renderRenameFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderReplaceSymbol() {
|
||||||
|
const { settings } = this.plugin;
|
||||||
|
new Setting(this.containerEl)
|
||||||
|
.setName('Replace pattern')
|
||||||
|
.setDesc('Files in this folder will be available renamed.')
|
||||||
|
.addText((textComponent) => {
|
||||||
|
textComponent.setValue(settings.existingSymbol);
|
||||||
|
textComponent.setPlaceholder('existing symbols');
|
||||||
|
textComponent.onChange((newValue) => {
|
||||||
|
settings.existingSymbol = newValue;
|
||||||
|
this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addText((textComponent) => {
|
||||||
|
textComponent.setValue(settings.replacePattern);
|
||||||
|
textComponent.setPlaceholder('replace with');
|
||||||
|
textComponent.onChange((newValue) => {
|
||||||
|
settings.replacePattern = newValue;
|
||||||
|
this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addButton((button) => {
|
||||||
|
button.setButtonText('Preview');
|
||||||
|
button.onClick(() => {
|
||||||
|
this.display();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
renderFileLocation() {
|
||||||
|
const { settings } = this.plugin;
|
||||||
|
new Setting(this.containerEl)
|
||||||
|
.setName('Folder location')
|
||||||
|
.setDesc('Files in this folder will be available renamed.')
|
||||||
|
.addSearch((cb) => {
|
||||||
|
new FolderSuggest(this.app, cb.inputEl);
|
||||||
|
cb.setPlaceholder('Example: folder1/')
|
||||||
|
.setValue(settings.folderName)
|
||||||
|
.onChange((newFolder) => {
|
||||||
|
settings.folderName = newFolder;
|
||||||
|
settings.fileNames = [...getObsidianFiles(this.app, newFolder)];
|
||||||
|
this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
// @ts-ignore
|
||||||
|
cb.containerEl.addClass('templater_search');
|
||||||
|
})
|
||||||
|
.addButton((button) => {
|
||||||
|
button.setButtonText('Refresh');
|
||||||
|
button.onClick(() => {
|
||||||
|
this.display();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
renderFilesAndPreview() {
|
||||||
|
const { settings } = this.plugin;
|
||||||
|
let existingFilesTextArea: HTMLTextAreaElement;
|
||||||
|
let replacedPreviewTextArea: HTMLTextAreaElement;
|
||||||
|
new Setting(this.containerEl)
|
||||||
|
.setName('files within the folder')
|
||||||
|
.setDesc(`Total Files: ${settings.fileNames.length}`)
|
||||||
|
.addTextArea((text) => {
|
||||||
|
existingFilesTextArea = text.inputEl;
|
||||||
|
const value = getRenderedFileNames(this.plugin);
|
||||||
|
text.setValue(value);
|
||||||
|
text.setDisabled(true);
|
||||||
|
const previewLabel = createPreviewElement();
|
||||||
|
text.inputEl.insertAdjacentElement('afterend', previewLabel);
|
||||||
|
})
|
||||||
|
.addTextArea((text) => {
|
||||||
|
replacedPreviewTextArea = text.inputEl;
|
||||||
|
const value = getRenderedFileNamesReplaced(this.plugin);
|
||||||
|
text.setValue(value);
|
||||||
|
text.setDisabled(true);
|
||||||
|
})
|
||||||
|
.then((setting) => {
|
||||||
|
syncScrolls(existingFilesTextArea, replacedPreviewTextArea, this.state);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRenameFiles() {
|
||||||
|
const { settings } = this.plugin;
|
||||||
|
new Setting(this.containerEl)
|
||||||
|
.setName('Replace pattern')
|
||||||
|
.setDesc('Files in this folder will be available renamed.')
|
||||||
|
.addButton((button) => {
|
||||||
|
button.setButtonText('Rename');
|
||||||
|
button.onClick(() => {
|
||||||
|
const { replacePattern, existingSymbol } = this.plugin.settings;
|
||||||
|
const firstFile = this.plugin.settings.fileNames[0];
|
||||||
|
console.log(firstFile);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.addText((cb) => {});
|
||||||
|
const fileManager = new FileManager();
|
||||||
|
this.plugin.settings.fileNames;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SampleSettingTab extends PluginSettingTab {
|
const getObsidianFiles = (app: App, folderName: string) => {
|
||||||
plugin: MyPlugin;
|
const abstractFiles = app.vault.getAllLoadedFiles();
|
||||||
|
const files = [] as TFile[];
|
||||||
|
abstractFiles.forEach((file) => {
|
||||||
|
if (file instanceof TFile && file.parent.name.includes(folderName)) {
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sortByname(files);
|
||||||
|
};
|
||||||
|
|
||||||
constructor(app: App, plugin: MyPlugin) {
|
const sortByname = (files: TFile[]) => {
|
||||||
super(app, plugin);
|
return files.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
this.plugin = plugin;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
display(): void {
|
const getRenderedFileNames = (plugin: MyPlugin) => {
|
||||||
const {containerEl} = this;
|
return prepareFileNameString(plugin.settings.fileNames);
|
||||||
|
};
|
||||||
|
|
||||||
containerEl.empty();
|
const prepareFileNameString = (filesNames: TFile[]) => {
|
||||||
|
let value = '';
|
||||||
|
filesNames.forEach((fileName, index) => {
|
||||||
|
const isLast = index + 1 === filesNames.length;
|
||||||
|
if (isLast) {
|
||||||
|
return (value += fileName.name);
|
||||||
|
}
|
||||||
|
value += fileName.name + '\r\n';
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
containerEl.createEl('h2', {text: 'Settings for my awesome plugin.'});
|
const getRenderedFileNamesReplaced = (plugin: MyPlugin) => {
|
||||||
|
const { fileNames, replacePattern, existingSymbol } = plugin.settings;
|
||||||
|
const newFiles = fileNames.map((file) => {
|
||||||
|
return {
|
||||||
|
...file,
|
||||||
|
name: file.name.replaceAll(existingSymbol, replacePattern),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return prepareFileNameString(newFiles);
|
||||||
|
};
|
||||||
|
|
||||||
new Setting(containerEl)
|
const createPreviewElement = () => {
|
||||||
.setName('Setting #1')
|
const previewLabel = window.document.createElement('span');
|
||||||
.setDesc('It\'s a secret')
|
previewLabel.className = 'previewLabel';
|
||||||
.addText(text => text
|
previewLabel.textContent = 'Preview';
|
||||||
.setPlaceholder('Enter your secret')
|
previewLabel.style.margin = '0 20px';
|
||||||
.setValue(this.plugin.settings.mySetting)
|
return previewLabel;
|
||||||
.onChange(async (value) => {
|
};
|
||||||
console.log('Secret: ' + value);
|
|
||||||
this.plugin.settings.mySetting = value;
|
const syncScrolls = (
|
||||||
await this.plugin.saveSettings();
|
existingFilesArea: HTMLTextAreaElement,
|
||||||
}));
|
previewArea: HTMLTextAreaElement,
|
||||||
}
|
state: State,
|
||||||
}
|
) => {
|
||||||
|
existingFilesArea.addEventListener('scroll', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (target.scrollTop !== state.previewScroll) {
|
||||||
|
previewArea.scrollTop = target.scrollTop;
|
||||||
|
state.previewScroll = target.scrollTop;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
previewArea.addEventListener('scroll', (event) => {
|
||||||
|
const target = event.target;
|
||||||
|
if (target.scrollTop !== state.filesScroll) {
|
||||||
|
existingFilesArea.scrollTop = target.scrollTop;
|
||||||
|
state.filesScroll = target.scrollTop;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"id": "obsidian-sample-plugin",
|
"id": "obsidian-bulk-rename-plugin",
|
||||||
"name": "Sample Plugin",
|
"name": "Bulk Rename",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "This is a sample plugin for Obsidian. This plugin demonstrates some of the capabilities of the Obsidian API.",
|
"description": "Purpose of this plugin rename files based on pattern",
|
||||||
"author": "Obsidian",
|
"author": "Obsidian",
|
||||||
"authorUrl": "https://obsidian.md",
|
"authorUrl": "https://obsidian.md",
|
||||||
"isDesktopOnly": false
|
"isDesktopOnly": false
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
|
@ -1,24 +1,28 @@
|
||||||
{
|
{
|
||||||
"name": "obsidian-sample-plugin",
|
"name": "obsidian-bulk-rename",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
"description": "Purpose of this plugin rename files based on pattern",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node esbuild.config.mjs",
|
"dev": "node esbuild.config.mjs",
|
||||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^16.11.6",
|
"@popperjs/core": "^2.11.2"
|
||||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
},
|
||||||
"@typescript-eslint/parser": "5.29.0",
|
"devDependencies": {
|
||||||
"builtin-modules": "3.3.0",
|
"@types/node": "^16.11.6",
|
||||||
"esbuild": "0.14.47",
|
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||||
"obsidian": "latest",
|
"@typescript-eslint/parser": "5.29.0",
|
||||||
"tslib": "2.4.0",
|
"builtin-modules": "3.3.0",
|
||||||
"typescript": "4.7.4"
|
"esbuild": "0.14.47",
|
||||||
}
|
"obsidian": "latest",
|
||||||
|
"tslib": "2.4.0",
|
||||||
|
"typescript": "4.7.4",
|
||||||
|
"prettier": "2.7.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
|
||||||
|
|
||||||
|
import { App, TAbstractFile, TextAreaComponent, TFile } from 'obsidian';
|
||||||
|
import { TextInputSuggest } from './suggest';
|
||||||
|
import TemplaterPlugin from 'main';
|
||||||
|
import { errorWrapperSync } from '../errors/errors';
|
||||||
|
import { getFilesFromTheFolder } from '../files/files';
|
||||||
|
|
||||||
|
export class FileSuggest extends TextInputSuggest<TFile> {
|
||||||
|
constructor(
|
||||||
|
public app: App,
|
||||||
|
public inputEl: any,
|
||||||
|
private plugin: TemplaterPlugin,
|
||||||
|
) {
|
||||||
|
super(app, inputEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFolder(): string {
|
||||||
|
return this.plugin.settings.mySetting;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_error_msg(): string {
|
||||||
|
return `Templates folder doesn't exist`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSuggestions(input_str: string): TFile[] {
|
||||||
|
const all_files = errorWrapperSync(
|
||||||
|
() => getFilesFromTheFolder(this.app, this.getFolder()),
|
||||||
|
this.get_error_msg(),
|
||||||
|
);
|
||||||
|
if (!all_files) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const files: TFile[] = [];
|
||||||
|
const lower_input_str = input_str.toLowerCase();
|
||||||
|
|
||||||
|
all_files.forEach((file: TAbstractFile) => {
|
||||||
|
if (
|
||||||
|
file instanceof TFile &&
|
||||||
|
file.extension === 'md' &&
|
||||||
|
file.path.toLowerCase().contains(lower_input_str)
|
||||||
|
) {
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSuggestion(file: TFile, el: HTMLElement): void {
|
||||||
|
el.setText(file.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectSuggestion(file: TFile): void {
|
||||||
|
this.inputEl.value = file.path;
|
||||||
|
this.inputEl.trigger('input');
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
// 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';
|
||||||
|
|
||||||
|
const wrapAround = (value: number, size: number): number => {
|
||||||
|
return ((value % size) + size) % size;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 | HTMLTextAreaElement;
|
||||||
|
|
||||||
|
private popper: PopperInstance;
|
||||||
|
private scope: Scope;
|
||||||
|
private suggestEl: HTMLElement;
|
||||||
|
private suggest: Suggest<T>;
|
||||||
|
|
||||||
|
constructor(app: App, inputEl: HTMLInputElement | HTMLTextAreaElement) {
|
||||||
|
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) {
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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([]);
|
||||||
|
if (this.popper) this.popper.destroy();
|
||||||
|
this.suggestEl.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getSuggestions(inputStr: string): T[];
|
||||||
|
abstract renderSuggestion(item: T, el: HTMLElement): void;
|
||||||
|
abstract selectSuggestion(item: T): void;
|
||||||
|
}
|
|
@ -10,15 +10,8 @@
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"lib": [
|
"lib": ["DOM", "ES5", "ES6", "ES7", "ES2021"]
|
||||||
"DOM",
|
|
||||||
"ES5",
|
|
||||||
"ES6",
|
|
||||||
"ES7"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["**/*.ts"]
|
||||||
"**/*.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue