mirror of
https://github.com/d0d1/obsidian-move-to-new-folder.git
synced 2026-07-22 06:06:40 +00:00
feat: scaffold plugin and implement move-to-new-folder workflow
This commit is contained in:
commit
751984280e
11 changed files with 1900 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
*.log
|
||||
7
README.md
Normal file
7
README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Move to New Folder
|
||||
|
||||
Move notes into newly created folders with a step-by-step workflow.
|
||||
|
||||
## Status
|
||||
|
||||
Implementation in progress.
|
||||
48
esbuild.config.mjs
Normal file
48
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
"/* THIS IS AN AUTOGENERATED/BUNDLED FILE BY ESBUILD */" +
|
||||
"\n" +
|
||||
"if (typeof window !== 'undefined' && window.require) { window.require = undefined; window.module = undefined; }";
|
||||
|
||||
const prod = process.argv[2] === "production";
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins,
|
||||
],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
await context.dispose();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await context.watch();
|
||||
475
main.js
Normal file
475
main.js
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
/* THIS IS AN AUTOGENERATED/BUNDLED FILE BY ESBUILD */
|
||||
if (typeof window !== 'undefined' && window.require) { window.require = undefined; window.module = undefined; }
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// main.ts
|
||||
var main_exports = {};
|
||||
__export(main_exports, {
|
||||
default: () => MoveToNewFolderPlugin
|
||||
});
|
||||
module.exports = __toCommonJS(main_exports);
|
||||
var import_obsidian = require("obsidian");
|
||||
var DEFAULT_SETTINGS = {
|
||||
defaultToCurrentParent: true
|
||||
};
|
||||
var ParentFolderPickerModal = class extends import_obsidian.Modal {
|
||||
constructor(app, initialPath, onCloseResolve) {
|
||||
super(app);
|
||||
this.searchValue = "";
|
||||
this.selectedIndex = 0;
|
||||
this.listByPath = /* @__PURE__ */ new Map();
|
||||
this.initialPath = initialPath;
|
||||
this.selectedPath = initialPath;
|
||||
this.onCloseResolve = onCloseResolve;
|
||||
this.folderPaths = this.collectFolderPaths();
|
||||
if (!this.folderPaths.includes(this.selectedPath)) {
|
||||
this.selectedPath = "";
|
||||
}
|
||||
}
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.modalEl.addClass("move-to-new-folder-modal");
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", { text: "Choose a parent folder" });
|
||||
contentEl.createEl("p", {
|
||||
text: "Search by typing or pick a folder from the list.",
|
||||
cls: "move-to-new-folder-hint"
|
||||
});
|
||||
const searchInput = contentEl.createEl("input", {
|
||||
type: "text",
|
||||
placeholder: "Search folders...",
|
||||
cls: "move-to-new-folder-search"
|
||||
});
|
||||
const listEl = contentEl.createDiv({ cls: "move-to-new-folder-list" });
|
||||
const render = () => {
|
||||
listEl.empty();
|
||||
this.listByPath.clear();
|
||||
const filtered = this.getFilteredFolders();
|
||||
if (filtered.length === 0) {
|
||||
listEl.createDiv({
|
||||
text: "No folders match your search.",
|
||||
cls: "move-to-new-folder-empty"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.selectedIndex >= filtered.length) {
|
||||
this.selectedIndex = 0;
|
||||
}
|
||||
const selectedByIndex = filtered[this.selectedIndex];
|
||||
this.selectedPath = selectedByIndex;
|
||||
for (const folderPath of filtered) {
|
||||
const button = listEl.createEl("button", {
|
||||
cls: "move-to-new-folder-item",
|
||||
text: folderPath.length > 0 ? folderPath : "/"
|
||||
});
|
||||
button.type = "button";
|
||||
button.dataset.path = folderPath;
|
||||
if (folderPath === this.selectedPath) {
|
||||
button.addClass("is-selected");
|
||||
}
|
||||
button.addEventListener("click", () => {
|
||||
this.selectedPath = folderPath;
|
||||
this.closeWithResult(folderPath);
|
||||
});
|
||||
button.addEventListener("mouseenter", () => {
|
||||
const idx = filtered.indexOf(folderPath);
|
||||
if (idx >= 0) {
|
||||
this.selectedIndex = idx;
|
||||
this.selectedPath = folderPath;
|
||||
this.refreshSelection(listEl, filtered);
|
||||
}
|
||||
});
|
||||
this.listByPath.set(folderPath, button);
|
||||
}
|
||||
this.refreshSelection(listEl, filtered);
|
||||
};
|
||||
searchInput.addEventListener("input", () => {
|
||||
this.searchValue = searchInput.value.trim();
|
||||
this.selectedIndex = 0;
|
||||
render();
|
||||
});
|
||||
searchInput.addEventListener("keydown", (event) => {
|
||||
const filtered = this.getFilteredFolders();
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
if (filtered.length > 0) {
|
||||
this.selectedIndex = Math.min(this.selectedIndex + 1, filtered.length - 1);
|
||||
this.selectedPath = filtered[this.selectedIndex];
|
||||
this.refreshSelection(listEl, filtered);
|
||||
}
|
||||
}
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
if (filtered.length > 0) {
|
||||
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
||||
this.selectedPath = filtered[this.selectedIndex];
|
||||
this.refreshSelection(listEl, filtered);
|
||||
}
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
if (filtered.length > 0) {
|
||||
this.closeWithResult(filtered[this.selectedIndex]);
|
||||
}
|
||||
}
|
||||
});
|
||||
const actionsEl = contentEl.createDiv({ cls: "move-to-new-folder-actions" });
|
||||
const cancelButton = actionsEl.createEl("button", {
|
||||
text: "Cancel",
|
||||
cls: "mod-muted"
|
||||
});
|
||||
cancelButton.type = "button";
|
||||
cancelButton.addEventListener("click", () => this.closeWithResult(null));
|
||||
const chooseButton = actionsEl.createEl("button", {
|
||||
text: "Choose",
|
||||
cls: "mod-cta"
|
||||
});
|
||||
chooseButton.type = "button";
|
||||
chooseButton.addEventListener("click", () => this.closeWithResult(this.selectedPath));
|
||||
render();
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
}
|
||||
onClose() {
|
||||
this.modalEl.removeClass("move-to-new-folder-modal");
|
||||
this.contentEl.empty();
|
||||
}
|
||||
collectFolderPaths() {
|
||||
const folderSet = /* @__PURE__ */ new Set([""]);
|
||||
for (const item of this.app.vault.getAllLoadedFiles()) {
|
||||
if (item instanceof import_obsidian.TFolder) {
|
||||
folderSet.add(item.path);
|
||||
}
|
||||
}
|
||||
return Array.from(folderSet).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
getFilteredFolders() {
|
||||
if (!this.searchValue) {
|
||||
return this.folderPaths;
|
||||
}
|
||||
const needle = this.searchValue.toLocaleLowerCase();
|
||||
return this.folderPaths.filter((path) => {
|
||||
const haystack = path.length > 0 ? path.toLocaleLowerCase() : "/";
|
||||
return haystack.includes(needle);
|
||||
});
|
||||
}
|
||||
refreshSelection(listEl, filtered) {
|
||||
for (const [path, button] of this.listByPath.entries()) {
|
||||
const shouldSelect = path === this.selectedPath;
|
||||
button.toggleClass("is-selected", shouldSelect);
|
||||
if (shouldSelect) {
|
||||
button.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
if (!filtered.includes(this.selectedPath) && filtered.length > 0) {
|
||||
this.selectedIndex = 0;
|
||||
this.selectedPath = filtered[0];
|
||||
this.refreshSelection(listEl, filtered);
|
||||
}
|
||||
}
|
||||
closeWithResult(selectedPath) {
|
||||
this.onCloseResolve({ selectedPath });
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
var TextPromptModal = class extends import_obsidian.Modal {
|
||||
constructor(app, title, placeholder, submitText, onCloseResolve) {
|
||||
super(app);
|
||||
this.title = title;
|
||||
this.placeholder = placeholder;
|
||||
this.submitText = submitText;
|
||||
this.onCloseResolve = onCloseResolve;
|
||||
}
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", { text: this.title });
|
||||
const input = contentEl.createEl("input", {
|
||||
type: "text",
|
||||
placeholder: this.placeholder
|
||||
});
|
||||
const hintEl = contentEl.createDiv({
|
||||
text: "Enter only a folder name, not a full path.",
|
||||
cls: "move-to-new-folder-hint"
|
||||
});
|
||||
const actionsEl = contentEl.createDiv({ cls: "move-to-new-folder-actions" });
|
||||
const cancelButton = actionsEl.createEl("button", {
|
||||
text: "Cancel",
|
||||
cls: "mod-muted"
|
||||
});
|
||||
cancelButton.type = "button";
|
||||
cancelButton.addEventListener("click", () => this.closeWithResult(null));
|
||||
const submitButton = actionsEl.createEl("button", {
|
||||
text: this.submitText,
|
||||
cls: "mod-cta"
|
||||
});
|
||||
submitButton.type = "button";
|
||||
const submit = () => {
|
||||
const value = input.value.trim();
|
||||
if (!value) {
|
||||
new import_obsidian.Notice("Folder name cannot be empty.");
|
||||
return;
|
||||
}
|
||||
if (value.includes("/") || value.includes("\\")) {
|
||||
hintEl.setText("Folder name cannot contain path separators.");
|
||||
return;
|
||||
}
|
||||
this.closeWithResult(value);
|
||||
};
|
||||
submitButton.addEventListener("click", submit);
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
submit();
|
||||
}
|
||||
});
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
closeWithResult(value) {
|
||||
this.onCloseResolve({ value });
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
var ConfirmationModal = class extends import_obsidian.Modal {
|
||||
constructor(app, titleText, bodyText, confirmText, onCloseResolve) {
|
||||
super(app);
|
||||
this.titleText = titleText;
|
||||
this.bodyText = bodyText;
|
||||
this.confirmText = confirmText;
|
||||
this.onCloseResolve = onCloseResolve;
|
||||
}
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", { text: this.titleText });
|
||||
contentEl.createEl("p", { text: this.bodyText });
|
||||
const actionsEl = contentEl.createDiv({ cls: "move-to-new-folder-actions" });
|
||||
const cancelButton = actionsEl.createEl("button", {
|
||||
text: "Cancel",
|
||||
cls: "mod-muted"
|
||||
});
|
||||
cancelButton.type = "button";
|
||||
cancelButton.addEventListener("click", () => this.closeWithResult(false));
|
||||
const confirmButton = actionsEl.createEl("button", {
|
||||
text: this.confirmText,
|
||||
cls: "mod-cta"
|
||||
});
|
||||
confirmButton.type = "button";
|
||||
confirmButton.addEventListener("click", () => this.closeWithResult(true));
|
||||
}
|
||||
onClose() {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
closeWithResult(confirmed) {
|
||||
this.onCloseResolve(confirmed);
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
var MoveToNewFolderSettingTab = class extends import_obsidian.PluginSettingTab {
|
||||
constructor(app, plugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
display() {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
containerEl.createEl("h2", { text: "Move to New Folder settings" });
|
||||
new import_obsidian.Setting(containerEl).setName("Default parent to current note folder").setDesc("When enabled, the folder picker starts at the current note's parent folder.").addToggle((toggle) => {
|
||||
toggle.setValue(this.plugin.settings.defaultToCurrentParent).onChange(async (value) => {
|
||||
this.plugin.settings.defaultToCurrentParent = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
var MoveToNewFolderPlugin = class extends import_obsidian.Plugin {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.settings = DEFAULT_SETTINGS;
|
||||
}
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new MoveToNewFolderSettingTab(this.app, this));
|
||||
this.addCommand({
|
||||
id: "move-note-to-new-folder",
|
||||
name: "Move note to new folder",
|
||||
checkCallback: (checking) => {
|
||||
const context = this.getActiveMarkdownFileContext();
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
if (!checking) {
|
||||
void this.runMoveFlow(context.file, context.leaf);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
this.registerFileMenuAction();
|
||||
});
|
||||
}
|
||||
async loadSettings() {
|
||||
const loaded = await this.loadData();
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded);
|
||||
}
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
registerFileMenuAction() {
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("file-menu", (menu, file, _source, leaf) => {
|
||||
if (!this.isMarkdownFile(file)) {
|
||||
return;
|
||||
}
|
||||
menu.addItem((item) => {
|
||||
item.setTitle("Move note to new folder").setIcon("folder-plus").onClick(() => {
|
||||
void this.runMoveFlow(file, leaf);
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
async runMoveFlow(file, leaf) {
|
||||
var _a, _b;
|
||||
const parentDefaultPath = this.settings.defaultToCurrentParent ? (_b = (_a = file.parent) == null ? void 0 : _a.path) != null ? _b : "" : "";
|
||||
const pickedParent = await this.pickParentFolder(parentDefaultPath);
|
||||
if (pickedParent === null) {
|
||||
return;
|
||||
}
|
||||
const newFolderName = await this.promptForFolderName();
|
||||
if (newFolderName === null) {
|
||||
return;
|
||||
}
|
||||
const targetFolderPath = (0, import_obsidian.normalizePath)(
|
||||
pickedParent.length > 0 ? `${pickedParent}/${newFolderName}` : newFolderName
|
||||
);
|
||||
const targetFolder = await this.ensureTargetFolder(targetFolderPath);
|
||||
if (!targetFolder) {
|
||||
return;
|
||||
}
|
||||
const targetFilePath = (0, import_obsidian.normalizePath)(`${targetFolder.path}/${file.name}`);
|
||||
const existingDestination = this.app.vault.getAbstractFileByPath(targetFilePath);
|
||||
if (existingDestination && existingDestination.path !== file.path) {
|
||||
new import_obsidian.Notice(`Move canceled: file already exists at "${targetFilePath}".`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.app.fileManager.renameFile(file, targetFilePath);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
new import_obsidian.Notice(`Could not move note: ${message}`);
|
||||
return;
|
||||
}
|
||||
const movedFile = this.app.vault.getAbstractFileByPath(targetFilePath);
|
||||
if (movedFile instanceof import_obsidian.TFile) {
|
||||
await this.openMovedFile(movedFile, leaf);
|
||||
new import_obsidian.Notice(`Moved to "${targetFolderPath}".`);
|
||||
return;
|
||||
}
|
||||
new import_obsidian.Notice(`Move completed, but could not reopen "${targetFilePath}".`);
|
||||
}
|
||||
async ensureTargetFolder(targetFolderPath) {
|
||||
const existing = this.app.vault.getAbstractFileByPath(targetFolderPath);
|
||||
if (existing instanceof import_obsidian.TFolder) {
|
||||
const confirmed = await this.confirmReuseFolder(targetFolderPath);
|
||||
if (!confirmed) {
|
||||
return null;
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
if (existing) {
|
||||
new import_obsidian.Notice(`Cannot use "${targetFolderPath}": a file already exists at that path.`);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
await this.app.vault.createFolder(targetFolderPath);
|
||||
} catch (error) {
|
||||
const raceWinner = this.app.vault.getAbstractFileByPath(targetFolderPath);
|
||||
if (raceWinner instanceof import_obsidian.TFolder) {
|
||||
return raceWinner;
|
||||
}
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
new import_obsidian.Notice(`Could not create folder: ${message}`);
|
||||
return null;
|
||||
}
|
||||
const created = this.app.vault.getAbstractFileByPath(targetFolderPath);
|
||||
if (created instanceof import_obsidian.TFolder) {
|
||||
return created;
|
||||
}
|
||||
new import_obsidian.Notice(`Folder creation succeeded but "${targetFolderPath}" is unavailable.`);
|
||||
return null;
|
||||
}
|
||||
async confirmReuseFolder(folderPath) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new ConfirmationModal(
|
||||
this.app,
|
||||
"Folder already exists",
|
||||
`Use existing folder "${folderPath}"?`,
|
||||
"Use folder",
|
||||
resolve
|
||||
);
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
async pickParentFolder(initialPath) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new ParentFolderPickerModal(this.app, initialPath, (result) => {
|
||||
resolve(result.selectedPath);
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
async promptForFolderName() {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new TextPromptModal(
|
||||
this.app,
|
||||
"Create a new folder",
|
||||
"New folder name",
|
||||
"Continue",
|
||||
(result) => {
|
||||
resolve(result.value);
|
||||
}
|
||||
);
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
async openMovedFile(file, leaf) {
|
||||
const targetLeaf = leaf != null ? leaf : this.app.workspace.getMostRecentLeaf();
|
||||
if (!targetLeaf) {
|
||||
return;
|
||||
}
|
||||
await targetLeaf.openFile(file, { active: true });
|
||||
}
|
||||
getActiveMarkdownFileContext() {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file || file.extension !== "md") {
|
||||
return null;
|
||||
}
|
||||
const leaf = this.app.workspace.getMostRecentLeaf();
|
||||
return { file, leaf: leaf != null ? leaf : void 0 };
|
||||
}
|
||||
isMarkdownFile(file) {
|
||||
return file instanceof import_obsidian.TFile && file.extension === "md";
|
||||
}
|
||||
};
|
||||
597
main.ts
Normal file
597
main.ts
Normal file
|
|
@ -0,0 +1,597 @@
|
|||
import {
|
||||
App,
|
||||
Menu,
|
||||
Modal,
|
||||
Notice,
|
||||
Plugin,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
TFolder,
|
||||
WorkspaceLeaf,
|
||||
normalizePath,
|
||||
} from "obsidian";
|
||||
|
||||
interface MoveToNewFolderSettings {
|
||||
defaultToCurrentParent: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: MoveToNewFolderSettings = {
|
||||
defaultToCurrentParent: true,
|
||||
};
|
||||
|
||||
interface ParentFolderPickerResult {
|
||||
selectedPath: string | null;
|
||||
}
|
||||
|
||||
interface TextPromptResult {
|
||||
value: string | null;
|
||||
}
|
||||
|
||||
class ParentFolderPickerModal extends Modal {
|
||||
private readonly initialPath: string;
|
||||
private readonly onCloseResolve: (result: ParentFolderPickerResult) => void;
|
||||
private searchValue = "";
|
||||
private selectedIndex = 0;
|
||||
private selectedPath: string;
|
||||
private readonly folderPaths: string[];
|
||||
private readonly listByPath: Map<string, HTMLButtonElement> = new Map();
|
||||
|
||||
constructor(app: App, initialPath: string, onCloseResolve: (result: ParentFolderPickerResult) => void) {
|
||||
super(app);
|
||||
this.initialPath = initialPath;
|
||||
this.selectedPath = initialPath;
|
||||
this.onCloseResolve = onCloseResolve;
|
||||
this.folderPaths = this.collectFolderPaths();
|
||||
|
||||
if (!this.folderPaths.includes(this.selectedPath)) {
|
||||
this.selectedPath = "";
|
||||
}
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
this.modalEl.addClass("move-to-new-folder-modal");
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", { text: "Choose a parent folder" });
|
||||
contentEl.createEl("p", {
|
||||
text: "Search by typing or pick a folder from the list.",
|
||||
cls: "move-to-new-folder-hint",
|
||||
});
|
||||
|
||||
const searchInput = contentEl.createEl("input", {
|
||||
type: "text",
|
||||
placeholder: "Search folders...",
|
||||
cls: "move-to-new-folder-search",
|
||||
});
|
||||
|
||||
const listEl = contentEl.createDiv({ cls: "move-to-new-folder-list" });
|
||||
|
||||
const render = (): void => {
|
||||
listEl.empty();
|
||||
this.listByPath.clear();
|
||||
|
||||
const filtered = this.getFilteredFolders();
|
||||
if (filtered.length === 0) {
|
||||
listEl.createDiv({
|
||||
text: "No folders match your search.",
|
||||
cls: "move-to-new-folder-empty",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedIndex >= filtered.length) {
|
||||
this.selectedIndex = 0;
|
||||
}
|
||||
|
||||
const selectedByIndex = filtered[this.selectedIndex];
|
||||
this.selectedPath = selectedByIndex;
|
||||
|
||||
for (const folderPath of filtered) {
|
||||
const button = listEl.createEl("button", {
|
||||
cls: "move-to-new-folder-item",
|
||||
text: folderPath.length > 0 ? folderPath : "/",
|
||||
});
|
||||
button.type = "button";
|
||||
button.dataset.path = folderPath;
|
||||
|
||||
if (folderPath === this.selectedPath) {
|
||||
button.addClass("is-selected");
|
||||
}
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
this.selectedPath = folderPath;
|
||||
this.closeWithResult(folderPath);
|
||||
});
|
||||
|
||||
button.addEventListener("mouseenter", () => {
|
||||
const idx = filtered.indexOf(folderPath);
|
||||
if (idx >= 0) {
|
||||
this.selectedIndex = idx;
|
||||
this.selectedPath = folderPath;
|
||||
this.refreshSelection(listEl, filtered);
|
||||
}
|
||||
});
|
||||
|
||||
this.listByPath.set(folderPath, button);
|
||||
}
|
||||
|
||||
this.refreshSelection(listEl, filtered);
|
||||
};
|
||||
|
||||
searchInput.addEventListener("input", () => {
|
||||
this.searchValue = searchInput.value.trim();
|
||||
this.selectedIndex = 0;
|
||||
render();
|
||||
});
|
||||
|
||||
searchInput.addEventListener("keydown", (event) => {
|
||||
const filtered = this.getFilteredFolders();
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
if (filtered.length > 0) {
|
||||
this.selectedIndex = Math.min(this.selectedIndex + 1, filtered.length - 1);
|
||||
this.selectedPath = filtered[this.selectedIndex];
|
||||
this.refreshSelection(listEl, filtered);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
if (filtered.length > 0) {
|
||||
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
||||
this.selectedPath = filtered[this.selectedIndex];
|
||||
this.refreshSelection(listEl, filtered);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
if (filtered.length > 0) {
|
||||
this.closeWithResult(filtered[this.selectedIndex]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const actionsEl = contentEl.createDiv({ cls: "move-to-new-folder-actions" });
|
||||
const cancelButton = actionsEl.createEl("button", {
|
||||
text: "Cancel",
|
||||
cls: "mod-muted",
|
||||
});
|
||||
cancelButton.type = "button";
|
||||
cancelButton.addEventListener("click", () => this.closeWithResult(null));
|
||||
|
||||
const chooseButton = actionsEl.createEl("button", {
|
||||
text: "Choose",
|
||||
cls: "mod-cta",
|
||||
});
|
||||
chooseButton.type = "button";
|
||||
chooseButton.addEventListener("click", () => this.closeWithResult(this.selectedPath));
|
||||
|
||||
render();
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.modalEl.removeClass("move-to-new-folder-modal");
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
private collectFolderPaths(): string[] {
|
||||
const folderSet = new Set<string>([""]);
|
||||
|
||||
for (const item of this.app.vault.getAllLoadedFiles()) {
|
||||
if (item instanceof TFolder) {
|
||||
folderSet.add(item.path);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(folderSet).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
private getFilteredFolders(): string[] {
|
||||
if (!this.searchValue) {
|
||||
return this.folderPaths;
|
||||
}
|
||||
|
||||
const needle = this.searchValue.toLocaleLowerCase();
|
||||
return this.folderPaths.filter((path) => {
|
||||
const haystack = path.length > 0 ? path.toLocaleLowerCase() : "/";
|
||||
return haystack.includes(needle);
|
||||
});
|
||||
}
|
||||
|
||||
private refreshSelection(listEl: HTMLElement, filtered: string[]): void {
|
||||
for (const [path, button] of this.listByPath.entries()) {
|
||||
const shouldSelect = path === this.selectedPath;
|
||||
button.toggleClass("is-selected", shouldSelect);
|
||||
if (shouldSelect) {
|
||||
button.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
|
||||
if (!filtered.includes(this.selectedPath) && filtered.length > 0) {
|
||||
this.selectedIndex = 0;
|
||||
this.selectedPath = filtered[0];
|
||||
this.refreshSelection(listEl, filtered);
|
||||
}
|
||||
}
|
||||
|
||||
private closeWithResult(selectedPath: string | null): void {
|
||||
this.onCloseResolve({ selectedPath });
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
class TextPromptModal extends Modal {
|
||||
private readonly title: string;
|
||||
private readonly placeholder: string;
|
||||
private readonly submitText: string;
|
||||
private readonly onCloseResolve: (result: TextPromptResult) => void;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
title: string,
|
||||
placeholder: string,
|
||||
submitText: string,
|
||||
onCloseResolve: (result: TextPromptResult) => void,
|
||||
) {
|
||||
super(app);
|
||||
this.title = title;
|
||||
this.placeholder = placeholder;
|
||||
this.submitText = submitText;
|
||||
this.onCloseResolve = onCloseResolve;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", { text: this.title });
|
||||
|
||||
const input = contentEl.createEl("input", {
|
||||
type: "text",
|
||||
placeholder: this.placeholder,
|
||||
});
|
||||
|
||||
const hintEl = contentEl.createDiv({
|
||||
text: "Enter only a folder name, not a full path.",
|
||||
cls: "move-to-new-folder-hint",
|
||||
});
|
||||
|
||||
const actionsEl = contentEl.createDiv({ cls: "move-to-new-folder-actions" });
|
||||
|
||||
const cancelButton = actionsEl.createEl("button", {
|
||||
text: "Cancel",
|
||||
cls: "mod-muted",
|
||||
});
|
||||
cancelButton.type = "button";
|
||||
cancelButton.addEventListener("click", () => this.closeWithResult(null));
|
||||
|
||||
const submitButton = actionsEl.createEl("button", {
|
||||
text: this.submitText,
|
||||
cls: "mod-cta",
|
||||
});
|
||||
submitButton.type = "button";
|
||||
|
||||
const submit = (): void => {
|
||||
const value = input.value.trim();
|
||||
if (!value) {
|
||||
new Notice("Folder name cannot be empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.includes("/") || value.includes("\\")) {
|
||||
hintEl.setText("Folder name cannot contain path separators.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeWithResult(value);
|
||||
};
|
||||
|
||||
submitButton.addEventListener("click", submit);
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
submit();
|
||||
}
|
||||
});
|
||||
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
private closeWithResult(value: string | null): void {
|
||||
this.onCloseResolve({ value });
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
class ConfirmationModal extends Modal {
|
||||
private readonly titleText: string;
|
||||
private readonly bodyText: string;
|
||||
private readonly confirmText: string;
|
||||
private readonly onCloseResolve: (confirmed: boolean) => void;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
titleText: string,
|
||||
bodyText: string,
|
||||
confirmText: string,
|
||||
onCloseResolve: (confirmed: boolean) => void,
|
||||
) {
|
||||
super(app);
|
||||
this.titleText = titleText;
|
||||
this.bodyText = bodyText;
|
||||
this.confirmText = confirmText;
|
||||
this.onCloseResolve = onCloseResolve;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl("h2", { text: this.titleText });
|
||||
contentEl.createEl("p", { text: this.bodyText });
|
||||
|
||||
const actionsEl = contentEl.createDiv({ cls: "move-to-new-folder-actions" });
|
||||
|
||||
const cancelButton = actionsEl.createEl("button", {
|
||||
text: "Cancel",
|
||||
cls: "mod-muted",
|
||||
});
|
||||
cancelButton.type = "button";
|
||||
cancelButton.addEventListener("click", () => this.closeWithResult(false));
|
||||
|
||||
const confirmButton = actionsEl.createEl("button", {
|
||||
text: this.confirmText,
|
||||
cls: "mod-cta",
|
||||
});
|
||||
confirmButton.type = "button";
|
||||
confirmButton.addEventListener("click", () => this.closeWithResult(true));
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
private closeWithResult(confirmed: boolean): void {
|
||||
this.onCloseResolve(confirmed);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
class MoveToNewFolderSettingTab extends PluginSettingTab {
|
||||
private readonly plugin: MoveToNewFolderPlugin;
|
||||
|
||||
constructor(app: App, plugin: MoveToNewFolderPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl("h2", { text: "Move to New Folder settings" });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Default parent to current note folder")
|
||||
.setDesc("When enabled, the folder picker starts at the current note's parent folder.")
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(this.plugin.settings.defaultToCurrentParent).onChange(async (value) => {
|
||||
this.plugin.settings.defaultToCurrentParent = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default class MoveToNewFolderPlugin extends Plugin {
|
||||
settings: MoveToNewFolderSettings = DEFAULT_SETTINGS;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
await this.loadSettings();
|
||||
|
||||
this.addSettingTab(new MoveToNewFolderSettingTab(this.app, this));
|
||||
|
||||
this.addCommand({
|
||||
id: "move-note-to-new-folder",
|
||||
name: "Move note to new folder",
|
||||
checkCallback: (checking) => {
|
||||
const context = this.getActiveMarkdownFileContext();
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!checking) {
|
||||
void this.runMoveFlow(context.file, context.leaf);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
this.registerFileMenuAction();
|
||||
});
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
const loaded = await this.loadData();
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded);
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
private registerFileMenuAction(): void {
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("file-menu", (menu: Menu, file: TAbstractFile, _source: string, leaf?: WorkspaceLeaf) => {
|
||||
if (!this.isMarkdownFile(file)) {
|
||||
return;
|
||||
}
|
||||
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle("Move note to new folder")
|
||||
.setIcon("folder-plus")
|
||||
.onClick(() => {
|
||||
void this.runMoveFlow(file, leaf);
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async runMoveFlow(file: TFile, leaf?: WorkspaceLeaf): Promise<void> {
|
||||
const parentDefaultPath = this.settings.defaultToCurrentParent ? file.parent?.path ?? "" : "";
|
||||
|
||||
const pickedParent = await this.pickParentFolder(parentDefaultPath);
|
||||
if (pickedParent === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newFolderName = await this.promptForFolderName();
|
||||
if (newFolderName === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetFolderPath = normalizePath(
|
||||
pickedParent.length > 0 ? `${pickedParent}/${newFolderName}` : newFolderName,
|
||||
);
|
||||
|
||||
const targetFolder = await this.ensureTargetFolder(targetFolderPath);
|
||||
if (!targetFolder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetFilePath = normalizePath(`${targetFolder.path}/${file.name}`);
|
||||
const existingDestination = this.app.vault.getAbstractFileByPath(targetFilePath);
|
||||
if (existingDestination && existingDestination.path !== file.path) {
|
||||
new Notice(`Move canceled: file already exists at "${targetFilePath}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.app.fileManager.renameFile(file, targetFilePath);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
new Notice(`Could not move note: ${message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const movedFile = this.app.vault.getAbstractFileByPath(targetFilePath);
|
||||
if (movedFile instanceof TFile) {
|
||||
await this.openMovedFile(movedFile, leaf);
|
||||
new Notice(`Moved to "${targetFolderPath}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
new Notice(`Move completed, but could not reopen "${targetFilePath}".`);
|
||||
}
|
||||
|
||||
private async ensureTargetFolder(targetFolderPath: string): Promise<TFolder | null> {
|
||||
const existing = this.app.vault.getAbstractFileByPath(targetFolderPath);
|
||||
|
||||
if (existing instanceof TFolder) {
|
||||
const confirmed = await this.confirmReuseFolder(targetFolderPath);
|
||||
if (!confirmed) {
|
||||
return null;
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
new Notice(`Cannot use "${targetFolderPath}": a file already exists at that path.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.app.vault.createFolder(targetFolderPath);
|
||||
} catch (error) {
|
||||
const raceWinner = this.app.vault.getAbstractFileByPath(targetFolderPath);
|
||||
if (raceWinner instanceof TFolder) {
|
||||
return raceWinner;
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
new Notice(`Could not create folder: ${message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const created = this.app.vault.getAbstractFileByPath(targetFolderPath);
|
||||
if (created instanceof TFolder) {
|
||||
return created;
|
||||
}
|
||||
|
||||
new Notice(`Folder creation succeeded but "${targetFolderPath}" is unavailable.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
private async confirmReuseFolder(folderPath: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new ConfirmationModal(
|
||||
this.app,
|
||||
"Folder already exists",
|
||||
`Use existing folder "${folderPath}"?`,
|
||||
"Use folder",
|
||||
resolve,
|
||||
);
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
|
||||
private async pickParentFolder(initialPath: string): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new ParentFolderPickerModal(this.app, initialPath, (result) => {
|
||||
resolve(result.selectedPath);
|
||||
});
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
|
||||
private async promptForFolderName(): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
const modal = new TextPromptModal(
|
||||
this.app,
|
||||
"Create a new folder",
|
||||
"New folder name",
|
||||
"Continue",
|
||||
(result) => {
|
||||
resolve(result.value);
|
||||
},
|
||||
);
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
|
||||
private async openMovedFile(file: TFile, leaf?: WorkspaceLeaf): Promise<void> {
|
||||
const targetLeaf = leaf ?? this.app.workspace.getMostRecentLeaf();
|
||||
if (!targetLeaf) {
|
||||
return;
|
||||
}
|
||||
|
||||
await targetLeaf.openFile(file, { active: true });
|
||||
}
|
||||
|
||||
private getActiveMarkdownFileContext(): { file: TFile; leaf?: WorkspaceLeaf } | null {
|
||||
const file = this.app.workspace.getActiveFile();
|
||||
if (!file || file.extension !== "md") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const leaf = this.app.workspace.getMostRecentLeaf();
|
||||
return { file, leaf: leaf ?? undefined };
|
||||
}
|
||||
|
||||
private isMarkdownFile(file: TAbstractFile): file is TFile {
|
||||
return file instanceof TFile && file.extension === "md";
|
||||
}
|
||||
}
|
||||
9
manifest.json
Normal file
9
manifest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "move-to-new-folder",
|
||||
"name": "Move to New Folder",
|
||||
"version": "1.0.0",
|
||||
"minAppVersion": "1.5.0",
|
||||
"description": "Move notes into newly created folders using a fast, step-by-step flow.",
|
||||
"author": "d0d1",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
665
package-lock.json
generated
Normal file
665
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,665 @@
|
|||
{
|
||||
"name": "move-to-new-folder",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "move-to-new-folder",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.17.57",
|
||||
"builtin-modules": "^4.0.0",
|
||||
"esbuild": "^0.25.3",
|
||||
"obsidian": "^1.8.10",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
|
||||
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.38.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
|
||||
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
||||
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
||||
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
||||
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
||||
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
|
||||
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/codemirror": {
|
||||
"version": "5.60.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
|
||||
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
|
||||
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tern": {
|
||||
"version": "0.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
||||
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz",
|
||||
"integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.12",
|
||||
"@esbuild/android-arm": "0.25.12",
|
||||
"@esbuild/android-arm64": "0.25.12",
|
||||
"@esbuild/android-x64": "0.25.12",
|
||||
"@esbuild/darwin-arm64": "0.25.12",
|
||||
"@esbuild/darwin-x64": "0.25.12",
|
||||
"@esbuild/freebsd-arm64": "0.25.12",
|
||||
"@esbuild/freebsd-x64": "0.25.12",
|
||||
"@esbuild/linux-arm": "0.25.12",
|
||||
"@esbuild/linux-arm64": "0.25.12",
|
||||
"@esbuild/linux-ia32": "0.25.12",
|
||||
"@esbuild/linux-loong64": "0.25.12",
|
||||
"@esbuild/linux-mips64el": "0.25.12",
|
||||
"@esbuild/linux-ppc64": "0.25.12",
|
||||
"@esbuild/linux-riscv64": "0.25.12",
|
||||
"@esbuild/linux-s390x": "0.25.12",
|
||||
"@esbuild/linux-x64": "0.25.12",
|
||||
"@esbuild/netbsd-arm64": "0.25.12",
|
||||
"@esbuild/netbsd-x64": "0.25.12",
|
||||
"@esbuild/openbsd-arm64": "0.25.12",
|
||||
"@esbuild/openbsd-x64": "0.25.12",
|
||||
"@esbuild/openharmony-arm64": "0.25.12",
|
||||
"@esbuild/sunos-x64": "0.25.12",
|
||||
"@esbuild/win32-arm64": "0.25.12",
|
||||
"@esbuild/win32-ia32": "0.25.12",
|
||||
"@esbuild/win32-x64": "0.25.12"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "1.12.3",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz",
|
||||
"integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/codemirror": "5.60.8",
|
||||
"moment": "2.29.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/state": "6.5.0",
|
||||
"@codemirror/view": "6.38.6"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
24
package.json
Normal file
24
package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "move-to-new-folder",
|
||||
"version": "1.0.0",
|
||||
"description": "Obsidian plugin for a better move-to-new-folder workflow",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc --noEmit --skipLibCheck && node esbuild.config.mjs production"
|
||||
},
|
||||
"keywords": [
|
||||
"obsidian",
|
||||
"plugin"
|
||||
],
|
||||
"author": "d0d1",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.17.57",
|
||||
"builtin-modules": "^4.0.0",
|
||||
"esbuild": "^0.25.3",
|
||||
"obsidian": "^1.8.10",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
48
styles.css
Normal file
48
styles.css
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
.move-to-new-folder-modal {
|
||||
width: min(640px, 92vw);
|
||||
}
|
||||
|
||||
.move-to-new-folder-modal .move-to-new-folder-hint {
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.move-to-new-folder-modal .move-to-new-folder-search {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.move-to-new-folder-modal .move-to-new-folder-list {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.move-to-new-folder-modal .move-to-new-folder-item {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-normal);
|
||||
border-radius: var(--radius-s);
|
||||
padding: 0.35rem 0.45rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.move-to-new-folder-modal .move-to-new-folder-item:hover,
|
||||
.move-to-new-folder-modal .move-to-new-folder-item.is-selected {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.move-to-new-folder-modal .move-to-new-folder-empty {
|
||||
color: var(--text-muted);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.move-to-new-folder-modal .move-to-new-folder-actions {
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES2020",
|
||||
"allowJs": false,
|
||||
"noImplicitAny": true,
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strict": true,
|
||||
"lib": [
|
||||
"DOM",
|
||||
"ES2020"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"1.0.0": "1.5.0"
|
||||
}
|
||||
Loading…
Reference in a new issue