mirror of
https://github.com/groldsf/obsidian_check_plugin.git
synced 2026-07-22 05:37:48 +00:00
Merge branch 'feature/flexible-checkbox-symbols' into dev
This commit is contained in:
commit
b4a789f04b
8 changed files with 1247 additions and 790 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,189 +0,0 @@
|
|||
import { CheckboxUtils } from "../src/checkboxUtils";
|
||||
|
||||
describe("CheckboxUtils old", () => {
|
||||
let checkboxUtilsXOnly: CheckboxUtils;
|
||||
let checkboxUtilsSpaceOnly: CheckboxUtils;
|
||||
|
||||
beforeEach(() => {
|
||||
checkboxUtilsXOnly = new CheckboxUtils({ xOnlyMode: true, enableAutomaticParentState: true, enableAutomaticChildState: true, });
|
||||
checkboxUtilsSpaceOnly = new CheckboxUtils({ xOnlyMode: false, enableAutomaticParentState: true, enableAutomaticChildState: true,});
|
||||
});
|
||||
|
||||
describe("findCheckboxesLine", () => {
|
||||
test("recognizes valid checkbox lines", () => {
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("- [ ] Task")).not.toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine(" - [ ] Task")).not.toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("- [x] Task")).not.toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("* [ ] Another task")).not.toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("+ [x] Task")).not.toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("1. [ ] Numbered task")).not.toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("1. [x] Numbered task")).not.toBeNull();
|
||||
});
|
||||
|
||||
test("correctly handles different checkbox states", () => {
|
||||
const states = [" ", "x", "-", "?", "!", "*", "#", "@", "[", "]", "y", "X", "Y"];
|
||||
|
||||
for (const state of states) {
|
||||
const lineInfo = checkboxUtilsXOnly.matchCheckboxLine(`- [${state}] Task`);
|
||||
expect(lineInfo).not.toBeNull();
|
||||
expect(lineInfo?.checkChar).toBe(state);
|
||||
}
|
||||
});
|
||||
|
||||
test("correctly handles indentation", () => {
|
||||
const noIndentMatch = checkboxUtilsXOnly.matchCheckboxLine("- [ ] Task");
|
||||
expect(noIndentMatch).not.toBeNull();
|
||||
expect(noIndentMatch!.indent).toBe("".length);
|
||||
|
||||
const singleSpaceMatch = checkboxUtilsXOnly.matchCheckboxLine(" - [ ] Task");
|
||||
expect(singleSpaceMatch).not.toBeNull();
|
||||
expect(singleSpaceMatch!.indent).toBe(" ".length);
|
||||
|
||||
const multiSpaceMatch = checkboxUtilsXOnly.matchCheckboxLine(" - [ ] Task");
|
||||
expect(multiSpaceMatch).not.toBeNull();
|
||||
expect(multiSpaceMatch!.indent).toBe(" ".length);
|
||||
|
||||
const tabMatch = checkboxUtilsXOnly.matchCheckboxLine("\t- [ ] Task");
|
||||
expect(tabMatch).not.toBeNull();
|
||||
expect(tabMatch!.indent).toBe("\t".length);
|
||||
});
|
||||
|
||||
test("ignores invalid lines", () => {
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("-[ ] Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("*[x] Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("1.[ ] Numbered task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("- [] Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine(" - Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("Just some text")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("1. Some numbered task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("")).toBeNull();
|
||||
});
|
||||
|
||||
test("ignores lines with missing opening or closing brackets", () => {
|
||||
// Missing opening bracket [
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("- ] Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("* ] Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("+ ] Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("1. ] Task")).toBeNull();
|
||||
|
||||
// Missing closing bracket ]
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("- [x Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("* [ Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("+ [- Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("1. [? Task")).toBeNull();
|
||||
|
||||
// Malformed with spaces
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("- [ x Task")).toBeNull();
|
||||
expect(checkboxUtilsXOnly.matchCheckboxLine("- x] Task")).toBeNull();
|
||||
});
|
||||
|
||||
test("extracts list marker correctly", () => {
|
||||
const dashMatch = checkboxUtilsXOnly.matchCheckboxLine("- [ ] Task");
|
||||
expect(dashMatch).not.toBeNull();
|
||||
expect(dashMatch!.marker).toBe("-");
|
||||
|
||||
const asteriskMatch = checkboxUtilsXOnly.matchCheckboxLine("* [ ] Task");
|
||||
expect(asteriskMatch).not.toBeNull();
|
||||
expect(asteriskMatch!.marker).toBe("*");
|
||||
|
||||
const plusMatch = checkboxUtilsXOnly.matchCheckboxLine("+ [ ] Task");
|
||||
expect(plusMatch).not.toBeNull();
|
||||
expect(plusMatch!.marker).toBe("+");
|
||||
|
||||
const numberedMatch = checkboxUtilsXOnly.matchCheckboxLine("1. [ ] Task");
|
||||
expect(numberedMatch).not.toBeNull();
|
||||
expect(numberedMatch!.marker).toBe("1.");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCheckedSymbol", () => {
|
||||
test("in xOnlyMode only 'x' is considered checked", () => {
|
||||
expect(checkboxUtilsXOnly.isCheckedSymbol("x")).toBe(true);
|
||||
expect(checkboxUtilsXOnly.isCheckedSymbol(" ")).toBe(false);
|
||||
expect(checkboxUtilsXOnly.isCheckedSymbol("-")).toBe(false);
|
||||
expect(checkboxUtilsXOnly.isCheckedSymbol("?")).toBe(false);
|
||||
});
|
||||
|
||||
test("in normal mode all symbols except space are considered checked", () => {
|
||||
expect(checkboxUtilsSpaceOnly.isCheckedSymbol("x")).toBe(true);
|
||||
expect(checkboxUtilsSpaceOnly.isCheckedSymbol(" ")).toBe(false);
|
||||
expect(checkboxUtilsSpaceOnly.isCheckedSymbol("-")).toBe(true);
|
||||
expect(checkboxUtilsSpaceOnly.isCheckedSymbol("?")).toBe(true);
|
||||
});
|
||||
|
||||
test("handles uppercase 'X' correctly", () => {
|
||||
expect(checkboxUtilsXOnly.isCheckedSymbol("X")).toBe(false);
|
||||
expect(checkboxUtilsSpaceOnly.isCheckedSymbol("X")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateSettings", () => {
|
||||
test("updates xOnlyMode setting correctly", () => {
|
||||
|
||||
const settings = { xOnlyMode: true, enableAutomaticParentState: true, enableAutomaticChildState: true,};
|
||||
const utils = new CheckboxUtils(settings);
|
||||
expect(utils.isCheckedSymbol("-")).toBe(false);
|
||||
|
||||
settings.xOnlyMode = false;
|
||||
expect(utils.isCheckedSymbol("-")).toBe(true);
|
||||
|
||||
settings.xOnlyMode = true;
|
||||
expect(utils.isCheckedSymbol("-")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncCheckboxes", () => {
|
||||
test("updates parent checkbox if all children are checked (xOnlyMode)", () => {
|
||||
const text = "- [ ] Parent\n - [x] Child 1\n - [x] Child 2";
|
||||
const expected = "- [x] Parent\n - [x] Child 1\n - [x] Child 2";
|
||||
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
|
||||
});
|
||||
|
||||
test("updates parent checkbox if all children are checked (spaceOnly)", () => {
|
||||
const text = "- [ ] Parent\n - [?] Child 1\n - [-] Child 2";
|
||||
const expected = "- [x] Parent\n - [?] Child 1\n - [-] Child 2";
|
||||
expect(checkboxUtilsSpaceOnly.propagateStateFromChildren(text)).toEqual(expected);
|
||||
});
|
||||
|
||||
test("unchecks parent if not all children are checked", () => {
|
||||
const text = "- [x] Parent\n - [x] Child 1\n - [ ] Child 2";
|
||||
const expected = "- [ ] Parent\n - [x] Child 1\n - [ ] Child 2";
|
||||
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
|
||||
});
|
||||
|
||||
test("doesn't change parent if it has no children", () => {
|
||||
const text = "- [x] Single";
|
||||
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(text);
|
||||
});
|
||||
|
||||
test("handles multiple levels of nesting correctly", () => {
|
||||
const text = "- [ ] Parent\n - [ ] Child\n - [x] Grandchild";
|
||||
const expected = "- [x] Parent\n - [x] Child\n - [x] Grandchild";
|
||||
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
|
||||
});
|
||||
|
||||
test("handles mixed indentation levels correctly", () => {
|
||||
const text = "- [ ] Parent\n - [x] Child 1\n - [x] Grandchild 1\n - [x] Child 2";
|
||||
const expected = "- [x] Parent\n - [x] Child 1\n - [x] Grandchild 1\n - [x] Child 2";
|
||||
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
|
||||
});
|
||||
|
||||
test("handles siblings with different indentation correctly", () => {
|
||||
const text = "- [ ] Parent\n - [x] Child 1\n - [x] Grandchild 1\n - [ ] Child 2";
|
||||
const expected = "- [ ] Parent\n - [x] Child 1\n - [x] Grandchild 1\n - [ ] Child 2";
|
||||
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
|
||||
});
|
||||
|
||||
test("handles multiple parents correctly", () => {
|
||||
const text = "- [ ] Parent 1\n - [x] Child 1\n- [ ] Parent 2\n - [x] Child 2";
|
||||
const expected = "- [x] Parent 1\n - [x] Child 1\n- [x] Parent 2\n - [x] Child 2";
|
||||
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
|
||||
});
|
||||
|
||||
test("handles mixed content with non-checkbox lines correctly", () => {
|
||||
const text = "- [ ] Parent\nSome regular text\n - [x] Child 1\nMore text\n - [x] Child 2";
|
||||
const expected = "- [ ] Parent\nSome regular text\n - [x] Child 1\nMore text\n - [x] Child 2";
|
||||
expect(checkboxUtilsXOnly.propagateStateFromChildren(text)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import CheckboxSyncPlugin from "./main";
|
||||
|
||||
export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
||||
plugin: CheckboxSyncPlugin;
|
||||
|
||||
constructor(app: App, plugin: CheckboxSyncPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("X-Only Mode")
|
||||
.setDesc("When enabled, only 'x' marks a task as complete. When disabled, any character except space marks a task as complete")
|
||||
.addToggle((toggle) =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.xOnlyMode)
|
||||
.onChange(async (value) => {
|
||||
await this.plugin.updateSettings(settings => {
|
||||
settings.xOnlyMode = value;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Update parent checkbox state automatically')
|
||||
.setDesc('If enabled, the state of a parent checkbox will be automatically updated based on the state of its children (all children checked = parent checked). If disabled, only manually changing a parent will affect its children.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableAutomaticParentState)
|
||||
.onChange(async (value) => {
|
||||
await this.plugin.updateSettings(settings => {
|
||||
settings.enableAutomaticParentState = value;
|
||||
});
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Update child checkbox state automatically')
|
||||
.setDesc('If enabled, changing the state of a parent checkbox will automatically update the state of all its direct and nested children. If disabled, changing a parent checkbox will not affect its children.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableAutomaticChildState)
|
||||
.onChange(async (value) => {
|
||||
await this.plugin.updateSettings(settings => {
|
||||
settings.enableAutomaticChildState = value;
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
import { CheckboxSyncPluginSettings } from "./types";
|
||||
import { CheckboxState, CheckboxSyncPluginSettings } from "./types";
|
||||
|
||||
export interface CheckboxLineInfo {
|
||||
indent: number;
|
||||
marker: string;
|
||||
checkChar: string;
|
||||
checkboxCharPosition: number;
|
||||
checkboxState: CheckboxState;
|
||||
isChecked: boolean | undefined;
|
||||
}
|
||||
|
||||
export class CheckboxUtils {
|
||||
|
|
@ -22,21 +24,37 @@ export class CheckboxUtils {
|
|||
const marker = match[2];
|
||||
const checkChar = match[3];
|
||||
const checkboxCharPosition = indent + marker.length + 2;
|
||||
const checkboxState = this.getCheckboxState(checkChar);
|
||||
let isChecked = checkboxState === CheckboxState.Ignore ? undefined : checkboxState === CheckboxState.Checked;
|
||||
|
||||
return {
|
||||
indent,
|
||||
marker,
|
||||
checkChar,
|
||||
checkboxCharPosition
|
||||
checkboxCharPosition,
|
||||
checkboxState,
|
||||
isChecked
|
||||
};
|
||||
}
|
||||
|
||||
isCheckedSymbol(text: string): boolean {
|
||||
return this.settings.xOnlyMode ? text === "x" : text !== " ";
|
||||
getCheckboxState(text: string): CheckboxState {
|
||||
if (this.settings.checkedSymbols.includes(text)) {
|
||||
return CheckboxState.Checked;
|
||||
}
|
||||
if (this.settings.uncheckedSymbols.includes(text)) {
|
||||
return CheckboxState.Unchecked;
|
||||
}
|
||||
if (this.settings.ignoreSymbols.includes(text)) {
|
||||
return CheckboxState.Ignore;
|
||||
}
|
||||
return this.settings.unknownSymbolPolicy;
|
||||
}
|
||||
|
||||
updateLineCheckboxStateWithInfo(line: string, shouldBeChecked: boolean, lineInfo: CheckboxLineInfo): string {
|
||||
const newCheckChar = shouldBeChecked ? "x" : " ";
|
||||
const checkedChar = this.settings.checkedSymbols.length > 0 ? this.settings.checkedSymbols[0] : 'x'; // 'x' как дефолт
|
||||
const uncheckedChar = this.settings.uncheckedSymbols.length > 0 ? this.settings.uncheckedSymbols[0] : ' '; // ' ' как дефолт
|
||||
|
||||
const newCheckChar = shouldBeChecked ? checkedChar : uncheckedChar;
|
||||
const pos = lineInfo.checkboxCharPosition;
|
||||
if (pos >= 0 && pos < line.length) {
|
||||
return line.substring(0, pos) + newCheckChar + line.substring(pos + 1);
|
||||
|
|
@ -52,18 +70,36 @@ export class CheckboxUtils {
|
|||
|
||||
const parentLineInfo = this.matchCheckboxLine(lines[parentLine]);
|
||||
if (!parentLineInfo) {
|
||||
console.log(`checkbox not found in line ${parentLine}`)
|
||||
console.warn(`checkbox not found in line ${parentLine}`)
|
||||
return text;
|
||||
}
|
||||
|
||||
const isChecked = this.isCheckedSymbol(parentLineInfo.checkChar);
|
||||
if (parentLineInfo.checkboxState === CheckboxState.Ignore) {
|
||||
return text;
|
||||
}
|
||||
let parentIsChecked = parentLineInfo.isChecked!;
|
||||
|
||||
let j = parentLine + 1;
|
||||
|
||||
while (j < lines.length) {
|
||||
const childLineInfo = this.matchCheckboxLine(lines[j]);
|
||||
const childText = lines[j];
|
||||
const childLineInfo = this.matchCheckboxLine(childText);
|
||||
if (!childLineInfo || childLineInfo.indent <= parentLineInfo.indent) break;
|
||||
lines[j] = this.updateLineCheckboxStateWithInfo(lines[j], isChecked, childLineInfo);
|
||||
j++;
|
||||
|
||||
if (childLineInfo.checkboxState !== CheckboxState.Ignore) {
|
||||
lines[j] = this.updateLineCheckboxStateWithInfo(lines[j], parentIsChecked, childLineInfo);
|
||||
j++;
|
||||
} else {
|
||||
//skip children ignore node
|
||||
j++;
|
||||
while (j < lines.length) {
|
||||
const subChildLineInfo = this.matchCheckboxLine(lines[j]);
|
||||
if (!subChildLineInfo || subChildLineInfo.indent <= childLineInfo.indent) {
|
||||
break;
|
||||
};
|
||||
j++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
|
@ -75,24 +111,37 @@ export class CheckboxUtils {
|
|||
const parentLineInfo = this.matchCheckboxLine(lines[i]);
|
||||
if (!parentLineInfo) continue;
|
||||
|
||||
const isChecked = this.isCheckedSymbol(parentLineInfo.checkChar);
|
||||
if (parentLineInfo.checkboxState === CheckboxState.Ignore) {
|
||||
continue;
|
||||
}
|
||||
const parentIsChecked = parentLineInfo.isChecked!;
|
||||
let allChildrenChecked = true;
|
||||
let hasChildren = false;
|
||||
let j = i + 1;
|
||||
|
||||
while (j < lines.length) {
|
||||
for (let j = i + 1; j < lines.length; j++) {
|
||||
const childLineInfo = this.matchCheckboxLine(lines[j]);
|
||||
|
||||
if (!childLineInfo || childLineInfo.indent <= parentLineInfo.indent) break;
|
||||
|
||||
if (childLineInfo.checkboxState === CheckboxState.Ignore) {
|
||||
while (j + 1 < lines.length) {
|
||||
const subChildLineInfo = this.matchCheckboxLine(lines[j + 1]);
|
||||
if (!subChildLineInfo || subChildLineInfo.indent <= childLineInfo.indent) {
|
||||
break;
|
||||
};
|
||||
j++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
hasChildren = true;
|
||||
const childrenIsChecked = this.isCheckedSymbol(childLineInfo.checkChar);
|
||||
const childrenIsChecked = childLineInfo.isChecked!;
|
||||
if (!childrenIsChecked) {
|
||||
allChildrenChecked = false;
|
||||
break;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
if (hasChildren && isChecked !== allChildrenChecked) {
|
||||
if (hasChildren && parentIsChecked !== allChildrenChecked) {
|
||||
lines[i] = this.updateLineCheckboxStateWithInfo(lines[i], allChildrenChecked, parentLineInfo);
|
||||
}
|
||||
}
|
||||
|
|
@ -137,7 +186,7 @@ export class CheckboxUtils {
|
|||
lineInfoBefore && lineInfoAfter &&
|
||||
lineInfoBefore.indent === lineInfoAfter.indent &&
|
||||
lineInfoBefore.marker === lineInfoAfter.marker &&
|
||||
lineInfoBefore.checkChar !== lineInfoAfter.checkChar
|
||||
lineInfoBefore.checkboxState !== lineInfoAfter.checkboxState
|
||||
) {
|
||||
return this.propagateStateToChildren(text, diffIndexes[0]);
|
||||
}
|
||||
|
|
|
|||
16
src/main.ts
16
src/main.ts
|
|
@ -1,17 +1,11 @@
|
|||
import { Plugin, TFile } from "obsidian";
|
||||
import { CheckboxSyncPluginSettingTab } from "./CheckboxSyncPluginSettingTab";
|
||||
import { CheckboxUtils } from "./checkboxUtils";
|
||||
import FileStateHolder from "./FileStateHolder";
|
||||
import SyncController from "./SyncController";
|
||||
import { CheckboxSyncPluginSettings } from "./types";
|
||||
import { FileLoadEventHandler } from "./events/FileLoadEventHandler";
|
||||
import { FileChangeEventHandler } from "./events/FileChangeEventHandler";
|
||||
|
||||
const DEFAULT_SETTINGS: CheckboxSyncPluginSettings = {
|
||||
xOnlyMode: true,
|
||||
enableAutomaticParentState: true,
|
||||
enableAutomaticChildState: true,
|
||||
};
|
||||
import { FileLoadEventHandler } from "./events/FileLoadEventHandler";
|
||||
import FileStateHolder from "./FileStateHolder";
|
||||
import { CheckboxSyncPluginSettingTab } from "./ui/CheckboxSyncPluginSettingTab";
|
||||
import SyncController from "./SyncController";
|
||||
import { CheckboxSyncPluginSettings, DEFAULT_SETTINGS } from "./types";
|
||||
|
||||
export default class CheckboxSyncPlugin extends Plugin {
|
||||
private _settings: CheckboxSyncPluginSettings;
|
||||
|
|
|
|||
22
src/types.ts
22
src/types.ts
|
|
@ -1,5 +1,23 @@
|
|||
export interface CheckboxSyncPluginSettings {
|
||||
xOnlyMode: boolean;
|
||||
enableAutomaticParentState: boolean;
|
||||
enableAutomaticChildState: boolean;
|
||||
}
|
||||
checkedSymbols: string[];
|
||||
uncheckedSymbols: string[];
|
||||
ignoreSymbols: string[];
|
||||
unknownSymbolPolicy: CheckboxState;
|
||||
}
|
||||
|
||||
export enum CheckboxState {
|
||||
Checked = 'checked',
|
||||
Unchecked = 'unchecked',
|
||||
Ignore = 'ignore'
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: CheckboxSyncPluginSettings = {
|
||||
enableAutomaticParentState: true,
|
||||
enableAutomaticChildState: true,
|
||||
checkedSymbols: ['x'],
|
||||
uncheckedSymbols: [' '],
|
||||
ignoreSymbols: [],
|
||||
unknownSymbolPolicy: CheckboxState.Checked,
|
||||
};
|
||||
409
src/ui/CheckboxSyncPluginSettingTab.ts
Normal file
409
src/ui/CheckboxSyncPluginSettingTab.ts
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
import { App, ButtonComponent, DropdownComponent, Notice, PluginSettingTab, Setting, TextComponent, ToggleComponent } from "obsidian";
|
||||
import CheckboxSyncPlugin from "../main";
|
||||
import { CheckboxState, DEFAULT_SETTINGS } from "../types";
|
||||
import { ConfirmModal, InfoModal, SaveConfirmModal } from "./modals";
|
||||
import { Mutex } from "async-mutex";
|
||||
|
||||
export class CheckboxSyncPluginSettingTab extends PluginSettingTab {
|
||||
plugin: CheckboxSyncPlugin;
|
||||
private checkedSymbolsInput: TextComponent;
|
||||
private uncheckedSymbolsInput: TextComponent;
|
||||
private ignoreSymbolsInput: TextComponent;
|
||||
private unknownPolicyDropdown: DropdownComponent;
|
||||
private parentToggle: ToggleComponent;
|
||||
private childToggle: ToggleComponent;
|
||||
private applyButton: ButtonComponent;
|
||||
private resetToDefaultButton: ButtonComponent;
|
||||
private errorDisplayEl: HTMLElement;
|
||||
private isDirty: boolean = false;
|
||||
private actionMutex = new Mutex();
|
||||
|
||||
constructor(app: App, plugin: CheckboxSyncPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a string expecting a JSON array of single-character strings.
|
||||
*/
|
||||
private parseJsonStringArray(value: string): { result: string[], error?: string } {
|
||||
if (value == null || typeof value !== 'string') {
|
||||
return { result: [], error: "Input must be a string." };
|
||||
}
|
||||
const trimmedValue = value.trim();
|
||||
if (trimmedValue === '') {
|
||||
return { result: [] };
|
||||
}
|
||||
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(trimmedValue);
|
||||
} catch (e: any) {
|
||||
return { result: [], error: `Invalid JSON format: ${e.message}` };
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
return { result: [], error: "Invalid format: Input must be a valid JSON array (e.g., [\"x\", \" \"] )." };
|
||||
}
|
||||
|
||||
const symbols = new Set<string>();
|
||||
for (let i = 0; i < parsed.length; i++) {
|
||||
const element = parsed[i];
|
||||
if (typeof element !== 'string') {
|
||||
return { result: [], error: `Invalid content: Array element at index ${i} is not a string.` };
|
||||
}
|
||||
if (element.length !== 1) {
|
||||
return { result: [], error: `Invalid content: Element "${element}" at index ${i} must be a single character.` };
|
||||
}
|
||||
symbols.add(element);
|
||||
}
|
||||
|
||||
return { result: [...symbols] }; // Возвращаем массив уникальных символов
|
||||
}
|
||||
|
||||
private arrayToJsonString(symbols: string[] | undefined): string {
|
||||
if (!symbols || symbols.length === 0) {
|
||||
return '[]';
|
||||
}
|
||||
try {
|
||||
// Используем форматирование с отступами для лучшей читаемости в TextArea
|
||||
return JSON.stringify(symbols);
|
||||
} catch (e) {
|
||||
// На случай, если в массиве окажется что-то несериализуемое (хотя не должно)
|
||||
console.error("Error stringifying array to JSON:", e);
|
||||
return '[]';
|
||||
}
|
||||
}
|
||||
|
||||
private validateSettingsArraysIntersection(checkedSymbols: string[], uncheckedSymbols: string[]): { isValid: boolean; error?: string } {
|
||||
const intersection = checkedSymbols.filter(symbol => uncheckedSymbols.includes(symbol));
|
||||
if (intersection.length > 0) {
|
||||
const displayIntersection = this.arrayToJsonString(intersection); // Используем JSON для вывода ошибки
|
||||
return { isValid: false, error: `Validation error: Symbol(s) found in both lists: ${displayIntersection}` };
|
||||
}
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
private settingChanged() {
|
||||
this.isDirty = true;
|
||||
this.applyButton.setDisabled(false).setCta();
|
||||
}
|
||||
|
||||
private settingSaved() {
|
||||
this.isDirty = false;
|
||||
this.applyButton.setDisabled(true).removeCta();
|
||||
}
|
||||
|
||||
display(): void {
|
||||
this.isDirty = false;
|
||||
const containerEl = this.containerEl;
|
||||
containerEl.empty();
|
||||
containerEl.createEl('h2', { text: 'Checkbox Sync Settings' });
|
||||
containerEl.createEl('h3', { text: 'Checkbox Symbol Configuration (Advanced: JSON)' });
|
||||
containerEl.createEl('p', { text: 'Warning: Requires valid JSON format. Use double quotes for strings.' });
|
||||
|
||||
// --- Checked Symbols ---
|
||||
new Setting(containerEl)
|
||||
.setName("Checked Symbols")
|
||||
.setDesc("Enter symbols as a JSON array of single-character strings. Example: [\"x\", \"✓\", \" \"]")
|
||||
.addText((text) => {
|
||||
this.checkedSymbolsInput = text;
|
||||
text
|
||||
.setPlaceholder("e.g., [\"x\", \" \"]")
|
||||
.setValue(this.arrayToJsonString(this.plugin.settings.checkedSymbols))
|
||||
.onChange(() => this.settingChanged())
|
||||
});
|
||||
|
||||
// --- Unchecked Symbols ---
|
||||
new Setting(containerEl)
|
||||
.setName("Unchecked Symbols")
|
||||
.setDesc("Enter symbols as a JSON array of single-character strings. Example: [\" \", \"?\", \",\"]")
|
||||
.addText((text) => {
|
||||
this.uncheckedSymbolsInput = text;
|
||||
text
|
||||
.setPlaceholder("e.g., [\" \", \"?\", \",\"]")
|
||||
.setValue(this.arrayToJsonString(this.plugin.settings.uncheckedSymbols))
|
||||
.onChange(() => this.settingChanged())
|
||||
});
|
||||
|
||||
// --- Ignore Symbols ---
|
||||
new Setting(containerEl)
|
||||
.setName("Ignore Symbols")
|
||||
.setDesc("Checkboxes with these symbols will be ignored during automatic parent/child state updates. Example: [\"-\", \"~\"]")
|
||||
.addText((text) => {
|
||||
this.ignoreSymbolsInput = text; // Сохраняем ссылку
|
||||
text
|
||||
.setPlaceholder("e.g., [\"-\", \"~\"]")
|
||||
.setValue(this.arrayToJsonString(this.plugin.settings.ignoreSymbols))
|
||||
.onChange(() => this.settingChanged())
|
||||
});
|
||||
|
||||
// --- Unknown Symbol Policy ---
|
||||
new Setting(containerEl)
|
||||
.setName('Unknown Symbol Policy')
|
||||
.setDesc('How to treat symbols not in Checked or Unchecked lists.')
|
||||
.addDropdown(dropdown => {
|
||||
this.unknownPolicyDropdown = dropdown;
|
||||
dropdown
|
||||
.addOption(CheckboxState.Checked, 'Treat as Checked')
|
||||
.addOption(CheckboxState.Unchecked, 'Treat as Unchecked')
|
||||
.addOption(CheckboxState.Ignore, 'Ignore')
|
||||
.setValue(this.plugin.settings.unknownSymbolPolicy)
|
||||
.onChange(() => this.settingChanged())
|
||||
});
|
||||
|
||||
|
||||
containerEl.createEl('h3', { text: 'Synchronization Behavior' });
|
||||
// --- Parent State Toggle ---
|
||||
new Setting(containerEl)
|
||||
.setName('Update parent checkbox state automatically')
|
||||
.setDesc('If enabled, the state of a parent checkbox will be automatically updated based on the state of its children (all children checked = parent checked). If disabled, only manually changing a parent will affect its children.')
|
||||
.addToggle(toggle => {
|
||||
this.parentToggle = toggle;
|
||||
toggle
|
||||
.setValue(this.plugin.settings.enableAutomaticParentState)
|
||||
.onChange(() => this.settingChanged())
|
||||
});
|
||||
|
||||
|
||||
// --- Child State Toggle ---
|
||||
new Setting(containerEl)
|
||||
.setName('Update child checkbox state automatically')
|
||||
.setDesc('If enabled, changing the state of a parent checkbox will automatically update the state of all its direct and nested children. If disabled, changing a parent checkbox will not affect its children.')
|
||||
.addToggle(toggle => {
|
||||
this.childToggle = toggle;
|
||||
toggle
|
||||
.setValue(this.plugin.settings.enableAutomaticChildState)
|
||||
.onChange(() => this.settingChanged())
|
||||
});
|
||||
|
||||
|
||||
// --- Область для вывода ошибок ---
|
||||
this.errorDisplayEl = containerEl.createDiv({ cls: 'checkbox-sync-settings-error' });
|
||||
// Используем стандартные CSS переменные Obsidian для цвета ошибки
|
||||
this.errorDisplayEl.style.color = 'var(--text-error)';
|
||||
this.errorDisplayEl.style.marginTop = '10px';
|
||||
this.errorDisplayEl.style.marginBottom = '10px';
|
||||
this.errorDisplayEl.style.minHeight = '1.5em'; // Резервируем место
|
||||
this.errorDisplayEl.style.whiteSpace = 'pre-wrap'; // Для переноса длинных ошибок
|
||||
this.errorDisplayEl.style.userSelect = 'text'; // или 'all'
|
||||
|
||||
// Используем Setting для группировки кнопок
|
||||
const buttonGroup = new Setting(containerEl)
|
||||
.setClass('checkbox-sync-button-group'); // Добавим класс для возможной стилизации
|
||||
|
||||
|
||||
buttonGroup.addButton(button => {
|
||||
button
|
||||
.setButtonText("Reset changes")
|
||||
.setTooltip("Revert changes to the last applied settings")
|
||||
.onClick(() => {
|
||||
this.resetInputsToSavedSettings();
|
||||
});
|
||||
});
|
||||
|
||||
buttonGroup.addButton(button => {
|
||||
this.resetToDefaultButton = button;
|
||||
button
|
||||
.setButtonText("Reset to defaults")
|
||||
.setTooltip("Reset all settings to default values and apply immediately")
|
||||
.onClick(() => {
|
||||
new ConfirmModal(this.app,
|
||||
"Reset all settings to default and apply immediately?\nThis cannot be undone.", // Добавил \n для переноса
|
||||
async () => { await this.applyDefaultSettings(); }
|
||||
).open();
|
||||
});
|
||||
});
|
||||
// --- Кнопка Применить ---
|
||||
buttonGroup
|
||||
.addButton(button => {
|
||||
this.applyButton = button;
|
||||
button
|
||||
.setButtonText("Apply Changes")
|
||||
.setDisabled(!this.isDirty)
|
||||
.onClick(async () => await this.applyChanges())
|
||||
});
|
||||
}
|
||||
|
||||
private async applyChanges() {
|
||||
await this.actionMutex.runExclusive(async () => {
|
||||
const originalButtonText = "Apply Changes";
|
||||
this.applyButton.setDisabled(true).setButtonText("Applying...").removeCta();
|
||||
|
||||
|
||||
try {
|
||||
await this.validateAndSaveSettings();
|
||||
|
||||
this.checkedSymbolsInput.setValue(this.arrayToJsonString(this.plugin.settings.checkedSymbols));
|
||||
this.uncheckedSymbolsInput.setValue(this.arrayToJsonString(this.plugin.settings.uncheckedSymbols));
|
||||
this.ignoreSymbolsInput.setValue(this.arrayToJsonString(this.plugin.settings.ignoreSymbols));
|
||||
|
||||
this.settingSaved();
|
||||
this.errorDisplayEl.setText('');
|
||||
new Notice("Checkbox Sync settings applied!", 3000);
|
||||
} catch (error: any) {
|
||||
// --- Ошибка ---
|
||||
console.error("Error applying checkbox sync settings:", error);
|
||||
// Выводим ошибку в специальный div
|
||||
this.errorDisplayEl.setText(`❌ ${error.message || "An unknown error occurred."}`);
|
||||
} finally {
|
||||
this.applyButton.setButtonText(originalButtonText); // Восстанавливаем текст
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the settings UI elements to reflect the currently saved plugin settings.
|
||||
*/
|
||||
private resetInputsToSavedSettings() {
|
||||
const settings = this.plugin.settings; // Получаем текущие сохраненные настройки
|
||||
|
||||
// Обновляем значения всех полей ввода
|
||||
this.checkedSymbolsInput.setValue(this.arrayToJsonString(settings.checkedSymbols));
|
||||
this.uncheckedSymbolsInput.setValue(this.arrayToJsonString(settings.uncheckedSymbols));
|
||||
this.ignoreSymbolsInput.setValue(this.arrayToJsonString(settings.ignoreSymbols));
|
||||
this.unknownPolicyDropdown.setValue(settings.unknownSymbolPolicy);
|
||||
this.parentToggle.setValue(settings.enableAutomaticParentState);
|
||||
this.childToggle.setValue(settings.enableAutomaticChildState);
|
||||
|
||||
// Очищаем ошибку и сбрасываем состояние "грязный"
|
||||
this.errorDisplayEl?.setText('');
|
||||
this.settingSaved(); // Используем существующий метод для сброса isDirty и состояния кнопки Apply
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the default settings immediately after confirmation.
|
||||
*/
|
||||
private async applyDefaultSettings() {
|
||||
await this.actionMutex.runExclusive(async () => {
|
||||
this.resetToDefaultButton.setDisabled(true).setButtonText("Resetting...");
|
||||
|
||||
this.errorDisplayEl.setText(''); // Очищаем предыдущие ошибки
|
||||
|
||||
try {
|
||||
const defaultsCopy = { ...DEFAULT_SETTINGS }; // Работаем с копией
|
||||
|
||||
await this.plugin.updateSettings(settings => {
|
||||
// Полностью перезаписываем текущие настройки дефолтными
|
||||
Object.assign(settings, defaultsCopy);
|
||||
});
|
||||
|
||||
// После успешного сохранения обновляем UI и состояние
|
||||
this.resetInputsToSavedSettings(); // Обновит UI по новым settings и сбросит isDirty/кнопку Apply
|
||||
|
||||
new Notice("Settings reset to defaults and applied.", 3000);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Error resetting settings to default:", error);
|
||||
this.errorDisplayEl.setText(`❌ Error resetting to defaults: ${error.message}`);
|
||||
} finally {
|
||||
// Восстанавливаем кнопку Reset
|
||||
this.resetToDefaultButton.setDisabled(false).setButtonText("Reset to defaults");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden hide method to handle unsaved changes.
|
||||
*/
|
||||
async hide() {
|
||||
if (this.isDirty) {
|
||||
// Колбэк для кнопки "Save"
|
||||
const saveCallback = async () => {
|
||||
try {
|
||||
await this.validateAndSaveSettings();
|
||||
this.isDirty = false; // Сбрасываем флаг
|
||||
new Notice("Settings saved.", 2000);
|
||||
} catch (error: any) {
|
||||
console.error("Error saving settings on hide:", error);
|
||||
let errorMessage = "An unknown error occurred.";
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
} else if (typeof error === 'string') {
|
||||
errorMessage = error;
|
||||
}
|
||||
// Открываем модалку с ошибкой
|
||||
new InfoModal(this.app, `Failed to save settings:\n\n${errorMessage}\n\nYour changes were not saved.`).open();
|
||||
}
|
||||
};
|
||||
|
||||
// Колбэк для кнопки "Discard"
|
||||
const discardCallback = () => {
|
||||
this.isDirty = false; // Считаем изменения отмененными
|
||||
};
|
||||
|
||||
// Открываем модалку Save/Discard и ЖДЕМ ее закрытия
|
||||
const saveModal = new SaveConfirmModal(this.app, saveCallback, discardCallback);
|
||||
saveModal.open();
|
||||
}
|
||||
// Продолжаем стандартное закрытие
|
||||
super.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads UI values, validates them, and calls plugin.updateSettings.
|
||||
* Throws an error if validation or saving fails.
|
||||
* Does NOT interact with UI feedback (buttons, notices, error display).
|
||||
*/
|
||||
private async validateAndSaveSettings(): Promise<void> {
|
||||
// 1. Считываем значения из UI
|
||||
const checkedValue = this.checkedSymbolsInput.getValue();
|
||||
const uncheckedValue = this.uncheckedSymbolsInput.getValue();
|
||||
const ignoreValue = this.ignoreSymbolsInput.getValue();
|
||||
const policyValue = this.unknownPolicyDropdown.getValue() as CheckboxState;
|
||||
const parentValue = this.parentToggle.getValue();
|
||||
const childValue = this.childToggle.getValue();
|
||||
|
||||
// 2. Парсим JSON
|
||||
const parsedChecked = this.parseJsonStringArray(checkedValue);
|
||||
const parsedUnchecked = this.parseJsonStringArray(uncheckedValue);
|
||||
const parsedIgnore = this.parseJsonStringArray(ignoreValue);
|
||||
|
||||
// 3. Проверяем ошибки парсинга -> throw
|
||||
if (parsedChecked.error) {
|
||||
throw new Error(`Checked Symbols Error: ${parsedChecked.error}`);
|
||||
}
|
||||
if (parsedUnchecked.error) {
|
||||
throw new Error(`Unchecked Symbols Error: ${parsedUnchecked.error}`);
|
||||
}
|
||||
if (parsedIgnore.error) {
|
||||
throw new Error(`Ignore Symbols Error: ${parsedIgnore.error}`);
|
||||
}
|
||||
|
||||
const checkedSymbolsArray = parsedChecked.result;
|
||||
const uncheckedSymbolsArray = parsedUnchecked.result;
|
||||
const ignoreSymbolsArray = parsedIgnore.result;
|
||||
|
||||
// 4. Валидируем пересечение -> throw
|
||||
const intersectionValidation1 = this.validateSettingsArraysIntersection(checkedSymbolsArray, uncheckedSymbolsArray);
|
||||
if (!intersectionValidation1.isValid) {
|
||||
throw new Error(`Lists: checked and unchecked. ${intersectionValidation1.error!}`);
|
||||
}
|
||||
const intersectionValidation2 = this.validateSettingsArraysIntersection(checkedSymbolsArray, ignoreSymbolsArray);
|
||||
if (!intersectionValidation2.isValid) {
|
||||
throw new Error(`Lists: checked and ignore. ${intersectionValidation2.error!}`);
|
||||
}
|
||||
const intersectionValidation3 = this.validateSettingsArraysIntersection(uncheckedSymbolsArray, ignoreSymbolsArray);
|
||||
if (!intersectionValidation3.isValid) {
|
||||
throw new Error(`Lists: unchecked and ignore. ${intersectionValidation3.error!}`);
|
||||
}
|
||||
|
||||
// 5. Валидируем на пустые списки -> throw
|
||||
if (checkedSymbolsArray.length === 0) {
|
||||
throw new Error("Checked symbols list cannot be empty.");
|
||||
}
|
||||
if (uncheckedSymbolsArray.length === 0) {
|
||||
throw new Error("Unchecked symbols list cannot be empty.");
|
||||
}
|
||||
|
||||
// 6. Вызываем сохранение -> await (может кинуть ошибку)
|
||||
await this.plugin.updateSettings(settings => {
|
||||
settings.checkedSymbols = checkedSymbolsArray;
|
||||
settings.uncheckedSymbols = uncheckedSymbolsArray;
|
||||
settings.ignoreSymbols = ignoreSymbolsArray;
|
||||
settings.unknownSymbolPolicy = policyValue;
|
||||
settings.enableAutomaticParentState = parentValue;
|
||||
settings.enableAutomaticChildState = childValue;
|
||||
});
|
||||
}
|
||||
}
|
||||
126
src/ui/modals.ts
Normal file
126
src/ui/modals.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
// Добавляем Modal в импорты из 'obsidian'
|
||||
import { App, Modal, Setting } from "obsidian";
|
||||
|
||||
// --- НОВЫЙ КОД: Класс ConfirmModal ---
|
||||
export class ConfirmModal extends Modal {
|
||||
message: string;
|
||||
onConfirm: () => void; // Синхронный колбэк
|
||||
|
||||
constructor(app: App, message: string, onConfirm: () => void) {
|
||||
super(app);
|
||||
this.message = message;
|
||||
this.onConfirm = onConfirm;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty(); // Очищаем на всякий случай
|
||||
contentEl.createEl('p').setText(this.message);
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText('Cancel')
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
}))
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText('OK') // Или "Confirm", "Yes"
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
this.onConfirm(); // Вызываем колбэк
|
||||
}));
|
||||
}
|
||||
|
||||
onClose() {
|
||||
let { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
// --- КОНЕЦ ConfirmModal ---
|
||||
|
||||
// --- НОВЫЙ КОД: Класс InfoModal ---
|
||||
export class InfoModal extends Modal {
|
||||
message: string;
|
||||
|
||||
constructor(app: App, message: string) {
|
||||
super(app);
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
// Используем <pre> для сохранения форматирования ошибки
|
||||
const pre = contentEl.createEl('pre');
|
||||
pre.setText(this.message);
|
||||
pre.style.whiteSpace = 'pre-wrap'; // Для переноса строк
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText('OK')
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
}));
|
||||
}
|
||||
|
||||
onClose() {
|
||||
let { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
// --- КОНЕЦ InfoModal ---
|
||||
|
||||
// --- НОВЫЙ КОД: Класс SaveConfirmModal ---
|
||||
export class SaveConfirmModal extends Modal {
|
||||
onSave: () => Promise<void>; // Асинхронный колбэк для сохранения
|
||||
onDiscard: () => void;
|
||||
|
||||
constructor(app: App, onSave: () => Promise<void>, onDiscard: () => void) {
|
||||
super(app);
|
||||
this.onSave = onSave;
|
||||
this.onDiscard = onDiscard;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.setText('You have unsaved changes. Save them before closing?');
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText('Discard')
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
this.onDiscard();
|
||||
}))
|
||||
.addButton((btn) =>
|
||||
btn
|
||||
.setButtonText('Save')
|
||||
.setCta()
|
||||
.onClick(async () => {
|
||||
btn.setDisabled(true).setButtonText('Saving...');
|
||||
try {
|
||||
await this.onSave();
|
||||
this.close();
|
||||
} catch (e) {
|
||||
console.error("Unexpected error during save callback:", e);
|
||||
new InfoModal(this.app, "An unexpected error occurred during save.").open();
|
||||
this.close(); // Закрываем в любом случае после ошибки
|
||||
} finally {
|
||||
// Восстанавливаем кнопку на случай если модалка не закроется (не должно быть)
|
||||
btn.setDisabled(false).setButtonText('Save');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
onClose() {
|
||||
let { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue