gtritchie_bulk-properties/src/main.ts
Gary Ritchie 62790c8ab8
Update npm dependencies to latest; adapt to TS 6 and Obsidian 1.13 typings (#53)
- Bump @types/node, esbuild, jiti, obsidian, typescript (6.0),
  and typescript-eslint to latest
- Hold @eslint/js at 9.x: eslint-plugin-obsidianmd peer-pins ^9.30.1
- tsconfig: drop unused baseUrl and switch moduleResolution to bundler
  (both deprecated as errors in TS 6.0)
- main.ts: declare settings to refine Plugin.settings (new in 1.13.0)
- settings.ts: add override to display(); centralise the still-supported
  imperative re-render behind rerender() with one deprecation suppression
- Keep setWarning() over setDestructive() to avoid raising minAppVersion
  from 1.8.7 to 1.13.0
2026-05-29 19:25:35 -06:00

192 lines
5.1 KiB
TypeScript

import {Plugin, TFile} from "obsidian";
import {BulkPropertiesSettingTab, BulkPropertiesSettings, DEFAULT_SETTINGS, PROPERTY_TYPES} from "./settings";
import {BulkEditModal} from "./bulk-edit-modal";
import {deselectAll} from "./deselect-all";
import {getSelectedFiles} from "./files";
import {removeSelectionProperty} from "./remove-selection-property";
import {isFileSelected, setSelection} from "./toggle-selection";
export default class BulkPropertiesPlugin extends Plugin {
declare settings: BulkPropertiesSettings;
private statusBarEl: HTMLElement | null = null;
private statusBarTimer: number | null = null;
private saveQueue: Promise<void> = Promise.resolve();
override async onload() {
await this.loadSettings();
this.statusBarEl = this.addStatusBarItem();
this.addRibbonIcon("list-checks", "Bulk edit selected notes", () => {
new BulkEditModal(this.app, this).open();
});
this.addCommand({
id: "bulk-edit-selected",
name: "Bulk edit selected notes",
callback: () => {
new BulkEditModal(this.app, this).open();
},
});
this.addCommand({
id: "deselect-all",
name: "Deselect all notes",
callback: () => {
void deselectAll(this);
},
});
this.addCommand({
id: "remove-selection-property",
name: "Remove selection property from all notes",
callback: () => {
removeSelectionProperty(this);
},
});
this.addSettingTab(new BulkPropertiesSettingTab(this.app, this));
this.registerEvent(
this.app.metadataCache.on("changed", () => {
this.debouncedUpdateStatusBar();
}),
);
this.registerEvent(
this.app.vault.on("delete", () => {
this.debouncedUpdateStatusBar();
}),
);
this.registerEvent(
this.app.workspace.on("editor-menu", (menu) => {
menu.addItem((item) => {
item.setTitle("Bulk edit selected notes")
.setIcon("list-checks")
.onClick(() => {
new BulkEditModal(this.app, this).open();
});
});
}),
);
this.registerEvent(
this.app.workspace.on("file-menu", (menu, file) => {
if (!(file instanceof TFile) || file.extension !== "md") return;
const selProp = this.settings.selectionProperty;
const selected = isFileSelected(this.app, file, selProp);
menu.addItem((item) => {
item.setTitle(selected ? "Deselect for bulk edit" : "Select for bulk edit")
.setIcon("list-checks")
.onClick(() => {
void setSelection(
this.app,
file,
selProp,
!selected,
() => this.updateStatusBar(),
);
});
});
}),
);
this.app.workspace.onLayoutReady(() => {
this.updateStatusBar();
});
}
override onunload() {
if (this.statusBarTimer !== null && this.statusBarEl) {
this.statusBarEl.win.clearTimeout(this.statusBarTimer);
this.statusBarTimer = null;
}
}
private debouncedUpdateStatusBar(): void {
if (!this.statusBarEl) return;
const win = this.statusBarEl.win;
if (this.statusBarTimer !== null) {
win.clearTimeout(this.statusBarTimer);
}
this.statusBarTimer = win.setTimeout(() => {
this.statusBarTimer = null;
this.updateStatusBar();
}, 500);
}
updateStatusBar(): void {
if (!this.statusBarEl) return;
if (!this.settings.showStatusBarCount) {
this.statusBarEl.empty();
return;
}
const count = getSelectedFiles(
this.app,
this.settings.selectionProperty,
).length;
if (count > 0) {
this.statusBarEl.setText(
`${count} note${count === 1 ? "" : "s"} selected`,
);
} else {
this.statusBarEl.empty();
}
}
async loadSettings() {
this.settings = Object.assign(
{}, DEFAULT_SETTINGS,
await this.loadData() as Partial<BulkPropertiesSettings>,
);
if (typeof this.settings.selectionProperty !== "string"
|| this.settings.selectionProperty.trim() === "") {
this.settings.selectionProperty = DEFAULT_SETTINGS.selectionProperty;
}
if (typeof this.settings.showLargeOperationWarning !== "boolean") {
this.settings.showLargeOperationWarning =
DEFAULT_SETTINGS.showLargeOperationWarning;
}
if (!Array.isArray(this.settings.properties)) {
this.settings.properties = [];
} else {
const validTypes: ReadonlySet<string> = new Set(PROPERTY_TYPES);
const before = this.settings.properties.length;
this.settings.properties = this.settings.properties.filter(
(p): p is typeof p =>
typeof p === "object"
&& p !== null
&& typeof p.name === "string"
&& p.name.trim() !== ""
&& validTypes.has(p.type),
);
if (this.settings.properties.length < before) {
console.warn(
`bulk-properties: discarded ${before - this.settings.properties.length} malformed property entries from settings`,
);
}
}
}
/**
* Serializes settings writes through a queue with copy-on-write
* semantics. Builds a candidate snapshot per call; only assigns
* it to `this.settings` after persistence succeeds.
*/
updateSetting<K extends keyof BulkPropertiesSettings>(
key: K,
value: BulkPropertiesSettings[K],
): Promise<void> {
const save = this.saveQueue.then(async () => {
const candidate = {...this.settings, [key]: value};
await this.saveData(candidate);
this.settings = candidate;
});
this.saveQueue = save.then(() => {}, () => {});
return save;
}
}