Merge pull request #202 from jsmorabito/eslint-fixes

ESLint cleanup: 266 errors to 0, plus bugs found along the way
This commit is contained in:
johnny1093 2026-07-11 14:05:40 -04:00 committed by GitHub
commit 474ba044dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 8511 additions and 1756 deletions

View file

@ -1,4 +0,0 @@
npm node_modules
build
scripts/
main.js

View file

@ -1,25 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "error",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": ["error"],
"semi": ["error", "always"],
"@typescript-eslint/class-literal-property-style": ["error", "fields"],
"@typescript-eslint/explicit-function-return-type": ["error"],
"@typescript-eslint/explicit-member-accessibility": ["error"]
}
}

89
eslint.config.mts Normal file
View file

@ -0,0 +1,89 @@
import tseslint from 'typescript-eslint';
import obsidianmd from 'eslint-plugin-obsidianmd';
import globals from 'globals';
import { defineConfig, globalIgnores } from 'eslint/config';
import { fileURLToPath } from 'url';
import path from 'path';
const dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig(
globalIgnores([
'node_modules',
'build',
'scripts',
'main.js',
'main.css',
'styles.css',
'tailwind.config.js',
]),
{
languageOptions: {
globals: {
...globals.browser,
},
parserOptions: {
projectService: {
allowDefaultProject: ['eslint.config.mts', 'manifest.json'],
},
tsconfigRootDir: dirname,
extraFileExtensions: ['.json'],
},
},
},
...tseslint.configs.recommended,
{
rules: {
'@typescript-eslint/ban-ts-comment': 'off',
'no-prototype-builtins': 'off',
'@typescript-eslint/no-empty-function': 'error',
'@typescript-eslint/class-literal-property-style': ['error', 'fields'],
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/explicit-member-accessibility': 'error',
},
},
...obsidianmd.configs.recommended,
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
// Preact discards the return value of JSX event handlers at
// runtime (unlike React's synthetic events), so async onClick
// handlers are safe here - only check non-attribute misuse
// (forEach callbacks, addEventListener, Promise executors, etc).
'@typescript-eslint/no-misused-promises': [
'error',
{ checksVoidReturn: { attributes: false } },
],
},
},
{
files: ['src/main.ts'],
rules: {
// Renaming this command's id would break existing users' saved hotkeys
'obsidianmd/commands/no-command-in-command-id': 'off',
},
},
{
files: ['eslint.config.mts', 'vitest.config.ts', 'src/__tests__/**'],
rules: {
// These only run under Node (eslint/vitest CLI), never bundled
// into main.js or loaded inside Obsidian, so the mobile/runtime
// compatibility rationale behind these rules doesn't apply here.
'obsidianmd/no-nodejs-modules': 'off',
'obsidianmd/no-global-this': 'off',
'obsidianmd/prefer-create-el': 'off',
},
},
{
files: ['package.json'],
rules: {
// builtin-modules, node-fetch, and npm-run-all are flagged as
// having more modern replacements. They're dev-only build
// tooling (used from scripts/, never bundled into main.js), so
// swapping them carries build-breakage risk for no end-user
// benefit. Deferred to a dedicated follow-up rather than bundled
// into this lint-cleanup branch.
'depend/ban-dependencies': 'off',
},
},
);

View file

@ -68,6 +68,7 @@
"Hide": "Skrýt",
"Hide other Commands": "Skrýt ostatní příkazy",
"Double click to enter custom value": "Dvakrát klikněte pro zadání vlastní hodnoty",
"Restore default": "Restore default",
"Choose custom spacing for Command Buttons": "Vyberte vlastní odsazení pro tlačítka příkazů",
"Change the spacing between commands. You can set different values on mobile and desktop.": "Změna odsazení mezi příkazy. Můžete nastavit různé hodnoty na mobilních a stolních zařízeních.",
"Warning": "Varování",

View file

@ -68,6 +68,7 @@
"Hide": "Verstecken",
"Hide other Commands": "Andere Befehle verstecken",
"Double click to enter custom value": "Doppelklicken um eigenen Wert einzutragen",
"Restore default": "Restore default",
"Choose custom spacing for Command Buttons": "Wähle den Abstand zwischen Befehlen",
"Change the spacing between commands. You can set different values on mobile and desktop.": "Verändert den Abstand zwischen Befehlen.",
"Warning": "Achtung",

View file

@ -68,6 +68,7 @@
"Hide": "Hide",
"Hide other Commands": "Hide other Commands",
"Double click to enter custom value": "Double click to enter custom value",
"Restore default": "Restore default",
"Choose custom spacing for Command Buttons": "Choose custom spacing for Command Buttons",
"Change the spacing between commands. You can set different values on mobile and desktop.": "Change the spacing between commands.",
"Warning": "Warning",

View file

@ -68,10 +68,11 @@
"Hide": "Masquer",
"Hide other Commands": "Masquer les autres commandes",
"Double click to enter custom value": "Double-cliquez pour entrer une valeur personnalisée",
"Restore default": "Restore default",
"Choose custom spacing for Command Buttons": "Choisissez un espacement personnalisé pour les boutons de commande",
"Change the spacing between commands.": "Modifier l'espacement entre les commandes.",
"Change the spacing between commands. You can set different values on mobile and desktop.": "Modifier l'espacement entre les commandes.",
"Warning": "Avertissement",
"As of Obsidian 0.16.0 you need to explicitly enable the Tab Title Bar. Once enabled, you might need to restart Obsidian.": "À partir d'Obsidian 0.16.0, vous devez activer explicitement la barre de titre des onglets. Une fois activée, il se peut que vous deviez redémarrer Obsidian.",
"As of Obsidian 0.16.0 you need to explicitly enable the View Header.": "À partir d'Obsidian 0.16.0, vous devez activer explicitement la barre de titre des onglets. Une fois activée, il se peut que vous deviez redémarrer Obsidian.",
"Open Appearance Settings": "Ouvrir les paramètres d'apparence",
"Explorer": "Explorateur"
}

View file

@ -68,6 +68,7 @@
"Hide": "Sakrij",
"Hide other Commands": "Sakrij druge naredbe",
"Double click to enter custom value": "Dvostruki klik za unos prilagođene vrijednosti",
"Restore default": "Restore default",
"Choose custom spacing for Command Buttons": "Odaberi prilagođeni razmak za gumbe naredbi",
"Change the spacing between commands. You can set different values on mobile and desktop.": "Promijeni razmak između naredbi.",
"Warning": "Upozorenje",

View file

@ -68,6 +68,7 @@
"Hide": "Verberg",
"Hide other Commands": "Verberg andere Commando's",
"Double click to enter custom value": "Dubbel klik om een aangepaste waarde in te vullen",
"Restore default": "Restore default",
"Choose custom spacing for Command Buttons": "Kies aangepaste regelafstand voor Commando Knoppen",
"Change the spacing between commands. You can set different values on mobile and desktop.": "Verander regelafstand tussen Commando's. Dit kan verschillen tussen mobiel en dekstop.",
"Warning": "Waarschuwing",

View file

@ -68,6 +68,7 @@
"Hide": "Спрятать",
"Hide other Commands": "Спрятать другие команды",
"Double click to enter custom value": "Дважды щелкните, чтобы ввести пользовательское значение",
"Restore default": "Restore default",
"Choose custom spacing for Command Buttons": "Выберите пользовательский интервал между командными кнопками",
"Change the spacing between commands. You can set different values on mobile and desktop.": "Измените расстояние между командами.",
"Warning": "Предупреждение",

View file

@ -68,6 +68,7 @@
"Hide": "Dölj",
"Hide other Commands": "Dölj andra kommandon",
"Double click to enter custom value": "Dubbelklicka för att fylla i valfritt värde",
"Restore default": "Restore default",
"Choose custom spacing for Command Buttons": "Välj valfri distans mellan kommandoknappar",
"Change the spacing between commands. You can set different values on mobile and desktop.": "Ändra distans mellan komandon. Du har möjlighet att sätta olika värden för stationära och mobila enheter.",
"Warning": "Varning",

View file

@ -68,6 +68,7 @@
"Hide": "Ẩm",
"Hide other Commands": "Ẩn các lệnh khác",
"Double click to enter custom value": "Nhấn đúp(x2) để thêm giá trị tuỳ chỉnh",
"Restore default": "Restore default",
"Choose custom spacing for Command Buttons": "Chọn độ giãn cách giữa các nút",
"Change the spacing between commands. You can set different values on mobile and desktop.": "Thay đổi khoảng các giữa các lệnh",
"Warning": "Cảnh báo",

View file

@ -68,6 +68,7 @@
"Hide": "隐藏",
"Hide other Commands": "隐藏其余命令",
"Double click to enter custom value": "双击以添加自定义值",
"Restore default": "Restore default",
"Choose custom spacing for Command Buttons": "为命令按钮选择自定义间距",
"Change the spacing between commands. You can set different values on mobile and desktop.": "改变命令之间的间距。你可以为移动和桌面设备设置不同的值。",
"Warning": "警告",

View file

@ -68,6 +68,7 @@
"Hide": "隱藏",
"Hide other Commands": "隱藏其他命令",
"Double click to enter custom value": "連按兩下以輸入自訂值",
"Restore default": "Restore default",
"Choose custom spacing for Command Buttons": "選擇命令按鈕的自訂間距",
"Change the spacing between commands. You can set different values on mobile and desktop.": "變更命令之間的間距。您可以為行動和桌面裝置設定不同的值。",
"Warning": "警告",

View file

@ -2,7 +2,7 @@
"id": "cmdr",
"name": "Commander",
"version": "0.5.5",
"minAppVersion": "1.4.0",
"minAppVersion": "1.4.4",
"description": "Customize your workspace by adding commands everywhere, create Macros and supercharge your mobile toolbar.",
"author": "jsmorabito & phibr0",
"authorUrl": "https://github.com/phibr0",

9067
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,10 @@
"build:css": "npx tailwindcss -i ./main.css -o ./styles.css --minify",
"build": "npm run build:esbuild && npm run build:css",
"dev:css": "npx tailwindcss -i ./main.css -o ./styles.css --watch",
"dev": "npm-run-all --parallel dev:*"
"dev": "npm-run-all --parallel dev:*",
"lint": "eslint .",
"test": "vitest run",
"test:watch": "vitest"
},
"keywords": [],
"author": "jsmorabito & phibr0",
@ -25,24 +28,32 @@
"@codemirror/search": "^6.2.3",
"@codemirror/state": "^6.1.4",
"@codemirror/view": "^6.6.0",
"@eslint/js": "^9.39.4",
"@eslint/json": "0.14.0",
"@types/canvas-confetti": "^1.6.0",
"@types/node": "^18.11.9",
"@types/react": "^18.0.25",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"@vitest/coverage-v8": "^4.1.9",
"builtin-modules": "^3.3.0",
"codemirror": "^6.0.1",
"codemirror6-themes": "^0.1.2",
"esbuild": "^0.28.1",
"esbuild-plugin-alias": "^0.2.1",
"esbuild-sass-plugin": "3.3.1",
"eslint": "^9.39.4",
"eslint-plugin-obsidianmd": "^0.4.1",
"globals": "^17.6.0",
"jiti": "^2.6.1",
"jsdom": "^29.1.1",
"node-fetch": "^3.3.0",
"npm-run-all": "^4.1.5",
"obsidian": "latest",
"sass-embedded": "^1.71.1",
"tailwindcss": "^3.2.4",
"tslib": "^2.4.1",
"typescript": "^4.9.3"
"typescript": "^4.9.3",
"typescript-eslint": "^8.59.1",
"vitest": "^4.1.9"
},
"dependencies": {
"array-move": "^4.0.0",
@ -58,7 +69,7 @@
"immutable": "^4.3.8",
"js-yaml": "^4.1.1",
"micromatch": "^4.0.8",
"minimatch": "^3.1.5",
"minimatch": "^9.0.5",
"nanoid": "^3.3.8",
"picomatch": "^2.3.2",
"postcss": "^8.5.10",

View file

@ -0,0 +1,3 @@
export default function t(str: string): string {
return str;
}

View file

@ -0,0 +1,49 @@
// Minimal Obsidian API mock for unit tests
// moment is a global in Obsidian; provide a minimal stub
(globalThis as Record<string, unknown>).moment = { locale: (): string => "en" };
export const Platform = {
isDesktop: true,
isMobile: false,
};
export function setIcon(el: HTMLElement, icon: string): void {
el.setAttribute("data-icon", icon);
}
export function requireApiVersion(_version: string): boolean {
return true;
}
export function getIconIds(): string[] {
return ["check", "x", "star", "home"];
}
export class Plugin {}
export class Modal {}
export class Setting {}
export class FuzzySuggestModal<T> {
public app: unknown;
protected declare items: T[];
public constructor(app: unknown) { this.app = app; }
// eslint-disable-next-line @typescript-eslint/no-empty-function -- minimal test double, no-op is intentional
public open(): void {}
// eslint-disable-next-line @typescript-eslint/no-empty-function -- minimal test double, no-op is intentional
public close(): void {}
}
export class SuggestModal<T> {
public app: unknown;
protected declare items: T[];
public constructor(app: unknown) { this.app = app; }
// eslint-disable-next-line @typescript-eslint/no-empty-function -- minimal test double, no-op is intentional
public open(): void {}
// eslint-disable-next-line @typescript-eslint/no-empty-function -- minimal test double, no-op is intentional
public close(): void {}
}
export class PluginSettingTab {
public app: unknown;
public plugin: unknown;
public containerEl: HTMLElement = document.createElement("div");
public constructor(app: unknown, plugin: unknown) { this.app = app; this.plugin = plugin; }
}

View file

@ -0,0 +1,91 @@
import { describe, it, expect, vi } from "vitest";
import { Action } from "../types";
import type { Macro } from "../types";
import type CommanderPlugin from "../main";
import CommanderPluginClass from "../main";
function buildPlugin(macros: Macro[]): CommanderPlugin {
const executeCommandById = vi.fn();
const plugin = {
settings: { macros },
app: {
commands: { executeCommandById },
},
} as unknown as CommanderPlugin;
plugin.executeMacro = CommanderPluginClass.prototype.executeMacro.bind(
plugin
) as CommanderPlugin["executeMacro"];
return plugin;
}
describe("executeMacro", () => {
it("throws when macro id does not exist", async () => {
const plugin = buildPlugin([]);
await expect(plugin.executeMacro(99)).rejects.toThrow("Macro not found");
});
it("executes COMMAND actions by calling executeCommandById", async () => {
const macro: Macro = {
name: "test",
icon: "check",
macro: [
{ action: Action.COMMAND, commandId: "editor:undo" },
{ action: Action.COMMAND, commandId: "editor:redo" },
],
};
const plugin = buildPlugin([macro]);
await plugin.executeMacro(0);
const spy = plugin.app.commands.executeCommandById as ReturnType<typeof vi.fn>;
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenNthCalledWith(1, "editor:undo");
expect(spy).toHaveBeenNthCalledWith(2, "editor:redo");
});
it("executes LOOP action the correct number of times", async () => {
const macro: Macro = {
name: "loop-test",
icon: "star",
macro: [{ action: Action.LOOP, times: 3, commandId: "some:command" }],
};
const plugin = buildPlugin([macro]);
await plugin.executeMacro(0);
const spy = plugin.app.commands.executeCommandById as ReturnType<typeof vi.fn>;
expect(spy).toHaveBeenCalledTimes(3);
});
it("handles DELAY action without throwing", async () => {
const macro: Macro = {
name: "delay-test",
icon: "home",
macro: [{ action: Action.DELAY, delay: 0 }],
};
const plugin = buildPlugin([macro]);
await expect(plugin.executeMacro(0)).resolves.toBeUndefined();
});
it("handles EDITOR action without throwing", async () => {
const macro: Macro = {
name: "editor-test",
icon: "home",
macro: [{ action: Action.EDITOR }],
};
const plugin = buildPlugin([macro]);
await expect(plugin.executeMacro(0)).resolves.toBeUndefined();
});
it("executes mixed action sequence in correct order", async () => {
const macro: Macro = {
name: "mixed",
icon: "check",
macro: [
{ action: Action.COMMAND, commandId: "cmd-1" },
{ action: Action.DELAY, delay: 0 },
{ action: Action.COMMAND, commandId: "cmd-2" },
],
};
const plugin = buildPlugin([macro]);
await plugin.executeMacro(0);
const spy = plugin.app.commands.executeCommandById as ReturnType<typeof vi.fn>;
expect(spy.mock.calls.map((c: string[]) => c[0])).toEqual(["cmd-1", "cmd-2"]);
});
});

View file

@ -0,0 +1,68 @@
import { describe, it, expect } from "vitest";
import { injectIcons } from "../util";
import type CommanderPlugin from "../main";
import type { AdvancedToolbarSettings } from "../types";
function makePlugin(commands: Record<string, { icon?: string }>): CommanderPlugin {
return {
app: { commands: { commands } },
} as unknown as CommanderPlugin;
}
function makeSettings(
mappedIcons: AdvancedToolbarSettings["mappedIcons"]
): AdvancedToolbarSettings {
return {
rowHeight: 48,
rowCount: 1,
spacing: 0,
buttonWidth: 48,
columnLayout: false,
mappedIcons,
tooltips: false,
heightOffset: 0,
};
}
describe("injectIcons", () => {
it("sets the icon on a matching command", () => {
const commands = { "my:command": { icon: "old-icon" } };
const settings = makeSettings([{ iconID: "star", commandID: "my:command" }]);
injectIcons(settings, makePlugin(commands));
expect(commands["my:command"].icon).toBe("star");
});
it("removes mappedIcon entry when command does not exist", () => {
const commands = {};
const settings = makeSettings([{ iconID: "star", commandID: "missing:command" }]);
injectIcons(settings, makePlugin(commands));
expect(settings.mappedIcons).toHaveLength(0);
});
it("leaves unrelated mapped icons intact", () => {
const commands = {
"cmd-a": { icon: "" },
"cmd-b": { icon: "" },
};
const settings = makeSettings([
{ iconID: "check", commandID: "cmd-a" },
{ iconID: "x", commandID: "cmd-b" },
]);
injectIcons(settings, makePlugin(commands));
expect(commands["cmd-a"].icon).toBe("check");
expect(commands["cmd-b"].icon).toBe("x");
expect(settings.mappedIcons).toHaveLength(2);
});
it("only prunes the missing command and keeps valid ones", () => {
const commands = { "exists:cmd": { icon: "" } };
const settings = makeSettings([
{ iconID: "star", commandID: "missing:cmd" },
{ iconID: "home", commandID: "exists:cmd" },
]);
injectIcons(settings, makePlugin(commands));
expect(settings.mappedIcons).toHaveLength(1);
expect(settings.mappedIcons[0].commandID).toBe("exists:cmd");
expect(commands["exists:cmd"].icon).toBe("home");
});
});

View file

@ -0,0 +1,43 @@
import { describe, it, expect } from "vitest";
import { isModeActive } from "../util";
import type CommanderPlugin from "../main";
function makePlugin(isMobile: boolean, appId = "desktop-app"): CommanderPlugin {
return {
app: { isMobile, appId },
} as unknown as CommanderPlugin;
}
describe("isModeActive", () => {
it('returns true for mode "any" on desktop', () => {
expect(isModeActive("any", makePlugin(false))).toBe(true);
});
it('returns true for mode "any" on mobile', () => {
expect(isModeActive("any", makePlugin(true))).toBe(true);
});
it('returns true for mode "desktop" when not mobile', () => {
expect(isModeActive("desktop", makePlugin(false))).toBe(true);
});
it('returns false for mode "desktop" when on mobile', () => {
expect(isModeActive("desktop", makePlugin(true))).toBe(false);
});
it('returns true for mode "mobile" when on mobile', () => {
expect(isModeActive("mobile", makePlugin(true))).toBe(true);
});
it('returns false for mode "mobile" when on desktop', () => {
expect(isModeActive("mobile", makePlugin(false))).toBe(false);
});
it("returns true when mode matches the appId", () => {
expect(isModeActive("my-vault", makePlugin(false, "my-vault"))).toBe(true);
});
it("returns false when mode does not match appId and is not any/mobile/desktop", () => {
expect(isModeActive("other-vault", makePlugin(false, "my-vault"))).toBe(false);
});
});

View file

@ -0,0 +1,62 @@
import { describe, it, expect } from "vitest";
import { readdirSync, readFileSync } from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const LOCALE_DIR = path.resolve(__dirname, "../../locale");
function loadJson(file: string): Record<string, string> {
return JSON.parse(
readFileSync(path.join(LOCALE_DIR, file), "utf-8")
) as Record<string, string>;
}
// Extract all {{placeholder}} tokens from a string
function placeholders(str: string): string[] {
return [...str.matchAll(/\{\{[^}]+\}\}/g)].map((m) => m[0]).sort();
}
const en = loadJson("en.json");
const enKeys = Object.keys(en);
const localeFiles = readdirSync(LOCALE_DIR).filter(
(f) => f.endsWith(".json") && f !== "en.json"
);
// Only validate locales that have been filled in (not empty stubs)
const filledLocales = localeFiles.filter((f) => {
const data = loadJson(f);
return Object.keys(data).length > 0;
});
describe("locale completeness (non-empty locales only)", () => {
it.each(filledLocales)("%s has no missing keys", (file) => {
const data = loadJson(file);
const missing = enKeys.filter((k) => !(k in data));
expect(missing, `Missing keys in ${file}`).toEqual([]);
});
it.each(filledLocales)("%s has no extra keys not in en.json", (file) => {
const data = loadJson(file);
const extra = Object.keys(data).filter((k) => !(k in en));
expect(extra, `Extra keys in ${file}`).toEqual([]);
});
it.each(filledLocales)("%s preserves all {{placeholders}}", (file) => {
const data = loadJson(file);
const mismatches: string[] = [];
for (const key of enKeys) {
if (!(key in data)) continue;
const expected = placeholders(en[key]);
const actual = placeholders(data[key]);
if (JSON.stringify(expected) !== JSON.stringify(actual)) {
mismatches.push(
`"${key}": expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
);
}
}
expect(mismatches, `Placeholder mismatches in ${file}`).toEqual([]);
});
});

2
src/__tests__/setup.ts Normal file
View file

@ -0,0 +1,2 @@
(globalThis as Record<string, unknown>).moment = { locale: (): string => "en" };
export {};

View file

@ -0,0 +1,79 @@
import { describe, it, expect, beforeEach } from "vitest";
import { updateStyles, removeStyles } from "../util";
import type { AdvancedToolbarSettings } from "../types";
const defaultSettings: AdvancedToolbarSettings = {
rowHeight: 48,
rowCount: 1,
spacing: 4,
buttonWidth: 48,
columnLayout: false,
mappedIcons: [],
tooltips: false,
heightOffset: 0,
};
beforeEach(() => {
document.body.className = "";
document.body.removeAttribute("style");
});
describe("updateStyles", () => {
it("sets CSS custom properties on document.body", () => {
updateStyles(defaultSettings);
const s = document.body.style;
expect(s.getPropertyValue("--at-button-height")).toBe("48px");
expect(s.getPropertyValue("--at-button-width")).toBe("48px");
expect(s.getPropertyValue("--at-row-count")).toBe("1");
expect(s.getPropertyValue("--at-spacing")).toBe("4px");
expect(s.getPropertyValue("--at-offset")).toBe("0px");
});
it("adds AT-row class when columnLayout is false", () => {
updateStyles({ ...defaultSettings, columnLayout: false });
expect(document.body.classList.contains("AT-row")).toBe(true);
expect(document.body.classList.contains("AT-column")).toBe(false);
});
it("adds AT-column class when columnLayout is true", () => {
updateStyles({ ...defaultSettings, columnLayout: true });
expect(document.body.classList.contains("AT-column")).toBe(true);
expect(document.body.classList.contains("AT-row")).toBe(false);
});
it("adds AT-multirow when rowCount > 1", () => {
updateStyles({ ...defaultSettings, rowCount: 3 });
expect(document.body.classList.contains("AT-multirow")).toBe(true);
});
it("does not add AT-multirow when rowCount === 1", () => {
updateStyles(defaultSettings);
expect(document.body.classList.contains("AT-multirow")).toBe(false);
});
it("adds AT-no-toolbar when rowCount === 0", () => {
updateStyles({ ...defaultSettings, rowCount: 0 });
expect(document.body.classList.contains("AT-no-toolbar")).toBe(true);
});
});
describe("removeStyles", () => {
it("removes CSS custom properties set by updateStyles", () => {
updateStyles(defaultSettings);
removeStyles();
const s = document.body.style;
expect(s.getPropertyValue("--at-button-height")).toBe("");
expect(s.getPropertyValue("--at-button-width")).toBe("");
expect(s.getPropertyValue("--at-row-count")).toBe("");
expect(s.getPropertyValue("--at-spacing")).toBe("");
expect(s.getPropertyValue("--at-offset")).toBe("");
});
it("removes layout classes set by updateStyles", () => {
updateStyles({ ...defaultSettings, rowCount: 2, columnLayout: true });
removeStyles();
expect(document.body.classList.contains("AT-multirow")).toBe(false);
expect(document.body.classList.contains("AT-row")).toBe(false);
expect(document.body.classList.contains("AT-column")).toBe(false);
});
});

View file

@ -43,7 +43,7 @@ export default class CommanderPlugin extends Plugin {
public async executeStartupMacros(): Promise<void> {
this.settings.macros.forEach((macro, idx) => {
if (macro.startup) {
this.executeMacro(idx);
void this.executeMacro(idx);
}
});
}
@ -55,12 +55,12 @@ export default class CommanderPlugin extends Plugin {
for (const command of macro.macro) {
switch (command.action) {
case Action.COMMAND: {
await this.app.commands.executeCommandById(command.commandId);
this.app.commands.executeCommandById(command.commandId);
continue;
}
case Action.DELAY: {
await new Promise((resolve) =>
setTimeout(resolve, command.delay)
window.setTimeout(resolve, command.delay)
);
continue;
}
@ -69,7 +69,7 @@ export default class CommanderPlugin extends Plugin {
}
case Action.LOOP: {
for (let i = 0; i < command.times; i++) {
await this.app.commands.executeCommandById(
this.app.commands.executeCommandById(
command.commandId
);
}
@ -129,7 +129,7 @@ export default class CommanderPlugin extends Plugin {
updateStyles(this.settings.advancedToolbar);
injectIcons(this.settings.advancedToolbar, this);
this.executeStartupMacros();
void this.executeStartupMacros();
// Push saved commands into the Text Toolbar plugin if it is installed
this.manager.textToolbarIntegration.reorder();
@ -142,7 +142,11 @@ export default class CommanderPlugin extends Plugin {
}
private async loadSettings(): Promise<void> {
const data = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
const data = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
) as CommanderSettings;
this.settings = data;
}
@ -151,11 +155,7 @@ export default class CommanderPlugin extends Plugin {
}
public listActiveToolbarCommands(): string[] {
//@ts-ignore
const activeCommands = this.app.vault.getConfig(
"mobileToolbarCommands"
);
return activeCommands;
return this.app.vault.getConfig("mobileToolbarCommands") as string[];
}
public getCommands(): Command[] {

View file

@ -10,10 +10,8 @@ export default abstract class CommandManagerBase {
this.pairs = pairArray;
}
// eslint-disable-next-line no-unused-vars
public abstract addCommand(pair: CommandIconPair): Promise<void> | void;
// eslint-disable-next-line no-unused-vars
public abstract removeCommand(pair: CommandIconPair): Promise<void> | void;
public abstract reorder(): Promise<void> | void;

View file

@ -128,7 +128,7 @@ export default class ExplorerManager extends CommandManagerBase {
this.plugin
).didChooseRemove())
) {
this.removeCommand(pair);
await this.removeCommand(pair);
}
};
};
@ -153,7 +153,7 @@ export default class ExplorerManager extends CommandManagerBase {
.setIcon("command")
.onClick(async () => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
await this.addCommand(pair);
});
})
.addSeparator()
@ -197,7 +197,7 @@ export default class ExplorerManager extends CommandManagerBase {
this.plugin
).didChooseRemove())
) {
this.removeCommand(pair);
await this.removeCommand(pair);
}
});
})
@ -221,7 +221,7 @@ export default class ExplorerManager extends CommandManagerBase {
}
action.addClass("cmdr-ribbon-removing");
action.addEventListener("transitionend", async () => {
action.addEventListener("transitionend", () => {
action.remove();
this.actions.delete(pair);
});

View file

@ -11,9 +11,9 @@ export default class LeftRibbonManager extends CommandManagerBase {
super(plugin, plugin.settings.leftRibbon);
this.plugin = plugin;
this.plugin.settings.leftRibbon.forEach((pair) =>
this.addCommand(pair, false)
);
this.plugin.settings.leftRibbon.forEach((pair) => {
void this.addCommand(pair, false);
});
this.plugin.app.workspace.onLayoutReady(() => {
// if (this.plugin.settings.showAddCommand) {
@ -36,10 +36,8 @@ export default class LeftRibbonManager extends CommandManagerBase {
this.plugin.addRibbonIcon(pair.icon, pair.name, () =>
this.plugin.app.commands.executeCommandById(pair.id)
);
// @ts-expect-error
const nativeAction = this.plugin.app.workspace.leftRibbon.items.find(
// @ts-expect-error
(i) => i.icon === pair.icon && i.name === i.name
(i) => i.icon === pair.icon && i.title === pair.name
);
if (nativeAction) {
nativeAction.buttonEl.style.color =
@ -59,23 +57,19 @@ export default class LeftRibbonManager extends CommandManagerBase {
this.plugin.settings.leftRibbon.remove(pair);
await this.plugin.saveSettings();
}
// @ts-expect-error
const nativeAction = this.plugin.app.workspace.leftRibbon.items.find(
// @ts-expect-error
(i) => i.icon === pair.icon && i.name === i.name
(i) => i.icon === pair.icon && i.title === pair.name
);
if (nativeAction) {
nativeAction.buttonEl.remove();
this.plugin.app.workspace.leftRibbon.items.remove(nativeAction);
}
// @ts-expect-error
app.workspace.leftRibbon.items.remove(nativeAction);
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
public reorder(): void {
this.plugin.settings.leftRibbon.forEach((pair) => {
this.removeCommand(pair, false);
this.addCommand(pair, false);
void this.removeCommand(pair, false);
void this.addCommand(pair, false);
});
}
}

View file

@ -29,28 +29,24 @@ abstract class Base extends CommandManagerBase {
}
// There is no state to update
// eslint-disable-next-line @typescript-eslint/no-empty-function
// eslint-disable-next-line @typescript-eslint/no-empty-function -- no state to update
public reorder(): void {}
protected addRemovableCommand(
// eslint-disable-next-line no-unused-vars
this: (_item: MenuItem) => void,
command: Command,
cmdPair: CommandIconPair,
plugin: CommanderPlugin,
menu: Menu,
commandList: CommandIconPair[]
// eslint-disable-next-line no-unused-vars
): (_item: MenuItem) => void {
return (item: MenuItem) => {
item.dom.addClass("cmdr");
item.dom.addClass("cmdr", "cmdr-menu-item");
item.dom.style.color =
cmdPair.color === "#000000" || cmdPair.color === undefined
? "inherit"
: cmdPair.color;
item.setSection("cmdr");
item.dom.style.display = "flex";
const optionEl = createDiv({
cls: "cmdr-menu-more-options",
});
@ -99,7 +95,7 @@ abstract class Base extends CommandManagerBase {
plugin
).didChooseRemove())
) {
removeMenu();
await removeMenu();
}
});
})
@ -120,10 +116,10 @@ abstract class Base extends CommandManagerBase {
let isRemovable = false;
const setNormal = (): void => {
optionEl.style.display = "none";
optionEl.removeClass("is-visible");
};
const setRemovable = (): void => {
optionEl.style.display = "block";
optionEl.addClass("is-visible");
};
const removeMenu = async (): Promise<void> => {
item.dom.addClass("cmdr-removing");
@ -164,9 +160,8 @@ abstract class Base extends CommandManagerBase {
const pair = await chooseNewCommand(plugin);
commandList.push(pair);
await plugin.saveSettings();
} catch (error) {
//Do some proper handling here
console.log(error);
} catch {
// User cancelled command/icon selection, nothing to do
}
});
});
@ -200,8 +195,7 @@ export class EditorMenuCommandManager extends Base {
continue;
menu.addItem(
this.addRemovableCommand.call(
this,
this.addRemovableCommand(
command,
cmdPair,
plugin,
@ -256,8 +250,7 @@ export class FileMenuCommandManager extends Base {
}
menu.addItem(
this.addRemovableCommand.call(
this,
this.addRemovableCommand(
command,
cmdPair,
plugin,

View file

@ -26,7 +26,7 @@ export default class PageHeaderManager extends CommandManagerBase {
const buttons = this.buttonsFor(leaf, true);
if (!buttons || buttons.has(id)) return;
const buttonIcon = (view as ItemView).addAction(icon, name, () => {
const buttonIcon = view.addAction(icon, name, () => {
this.plugin.app.workspace.setActiveLeaf(leaf, { focus: true });
this.plugin.app.commands.executeCommandById(id);
});
@ -45,7 +45,7 @@ export default class PageHeaderManager extends CommandManagerBase {
.setIcon("command")
.onClick(async () => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
await this.addCommand(pair);
});
})
.addSeparator()
@ -59,7 +59,7 @@ export default class PageHeaderManager extends CommandManagerBase {
if (newIcon && newIcon !== pair.icon) {
pair.icon = newIcon;
await this.plugin.saveSettings();
this.reorder();
await this.reorder();
}
});
})
@ -74,7 +74,7 @@ export default class PageHeaderManager extends CommandManagerBase {
if (newName && newName !== pair.name) {
pair.name = newName;
await this.plugin.saveSettings();
this.reorder();
await this.reorder();
}
});
})
@ -89,7 +89,7 @@ export default class PageHeaderManager extends CommandManagerBase {
this.plugin
).didChooseRemove())
) {
this.removeCommand(pair);
await this.removeCommand(pair);
}
});
})
@ -108,7 +108,7 @@ export default class PageHeaderManager extends CommandManagerBase {
})
);
this.plugin.app.workspace.onLayoutReady(() =>
setTimeout(() => this.addButtonsToAllLeaves(), 100)
window.setTimeout(() => this.addButtonsToAllLeaves(), 100)
);
}
@ -118,7 +118,7 @@ export default class PageHeaderManager extends CommandManagerBase {
if (!(view instanceof ItemView)) return;
if (this.buttons.get(view)?.has(id)) return;
const buttonIcon = view.addAction("plus", t("Add new"), async () => {
this.addCommand(await chooseNewCommand(this.plugin));
await this.addCommand(await chooseNewCommand(this.plugin));
});
buttonIcon.addClasses(["cmdr", id]);
if (!this.buttons.has(view)) this.buttons.set(view, new Map());
@ -126,7 +126,7 @@ export default class PageHeaderManager extends CommandManagerBase {
}
private addButtonsToAllLeaves(refresh = false): void {
activeWindow.requestAnimationFrame(() =>
window.requestAnimationFrame(() =>
this.plugin.app.workspace.iterateAllLeaves((leaf) =>
this.addButtonsToLeaf(leaf, refresh)
)
@ -134,7 +134,7 @@ export default class PageHeaderManager extends CommandManagerBase {
}
private removeButtonsFromAllLeaves(): void {
activeWindow.requestAnimationFrame(() =>
window.requestAnimationFrame(() =>
this.plugin.app.workspace.iterateAllLeaves((leaf) =>
this.removeButtonsFromLeaf(leaf)
)

View file

@ -52,7 +52,7 @@ export default class StatusBarManager extends CommandManagerBase {
const pair = await chooseNewCommand(
this.plugin
);
this.addCommand(pair);
await this.addCommand(pair);
});
})
.showAtMouseEvent(event);
@ -63,7 +63,7 @@ export default class StatusBarManager extends CommandManagerBase {
setIcon(this.addBtn, "plus");
this.addBtn.onclick = async (): Promise<void> => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
await this.addCommand(pair);
this.reorder();
};
if (this.plugin.settings.showAddCommand)
@ -118,7 +118,7 @@ export default class StatusBarManager extends CommandManagerBase {
this.plugin
).didChooseRemove())
) {
this.removeCommand(pair);
await this.removeCommand(pair);
}
};
};
@ -143,7 +143,7 @@ export default class StatusBarManager extends CommandManagerBase {
.setIcon("command")
.onClick(async () => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
await this.addCommand(pair);
});
})
.addSeparator()
@ -187,7 +187,7 @@ export default class StatusBarManager extends CommandManagerBase {
this.plugin
).didChooseRemove())
) {
this.removeCommand(pair);
await this.removeCommand(pair);
}
});
})
@ -209,7 +209,7 @@ export default class StatusBarManager extends CommandManagerBase {
}
action.addClass("cmdr-ribbon-removing");
action.addEventListener("transitionend", async () => {
action.addEventListener("transitionend", () => {
action.remove();
this.actions.delete(pair);
});

View file

@ -3,13 +3,14 @@ import { CommandIconPair } from "src/types";
import CommandManagerBase from "./commandManager";
interface TextToolbarAPI {
// eslint-disable-next-line no-unused-vars
setCommands(_cmds: { id: string; icon: string; name: string }[]): void;
}
function getTextToolbarAPI(plugin: CommanderPlugin): TextToolbarAPI | undefined {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (plugin.app as any).plugins?.plugins?.["text-formatting-toolbar"]?.api;
const app = plugin.app as unknown as {
plugins?: { plugins?: Record<string, { api?: TextToolbarAPI }> };
};
return app.plugins?.plugins?.["text-formatting-toolbar"]?.api;
}
export default class TextToolbarIntegrationManager extends CommandManagerBase {

View file

@ -26,7 +26,6 @@ export default class TitleBarManager extends CommandManagerBase {
private init(): void {
this.plugin.app.workspace.onLayoutReady(async () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.container = document.querySelector(
".titlebar div.titlebar-button-container.mod-right"
)!;
@ -37,17 +36,17 @@ export default class TitleBarManager extends CommandManagerBase {
}
if (isModeActive(cmd.mode, this.plugin)) {
this.addTitleBarAction(cmd);
await this.addTitleBarAction(cmd);
}
}
this.plugin.saveSettings();
await this.plugin.saveSettings();
this.plugin.register(() => this.addBtn.remove());
this.addBtn.style.setProperty("--icon-size", `12px`);
setIcon(this.addBtn, "plus");
this.addBtn.onclick = async (): Promise<void> => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
await this.addCommand(pair);
this.reorder();
};
if (this.plugin.settings.showAddCommand)
@ -63,7 +62,7 @@ export default class TitleBarManager extends CommandManagerBase {
public async addCommand(pair: CommandIconPair): Promise<void> {
this.pairs.push(pair);
this.addTitleBarAction(pair);
await this.addTitleBarAction(pair);
await this.plugin.saveSettings();
}
@ -92,7 +91,7 @@ export default class TitleBarManager extends CommandManagerBase {
this.plugin
).didChooseRemove())
) {
this.removeCommand(pair);
await this.removeCommand(pair);
}
};
};
@ -117,7 +116,7 @@ export default class TitleBarManager extends CommandManagerBase {
.setIcon("command")
.onClick(async () => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
await this.addCommand(pair);
});
})
.addSeparator()
@ -161,7 +160,7 @@ export default class TitleBarManager extends CommandManagerBase {
this.plugin
).didChooseRemove())
) {
this.removeCommand(pair);
await this.removeCommand(pair);
}
});
})
@ -189,7 +188,7 @@ export default class TitleBarManager extends CommandManagerBase {
}
action.addClass("cmdr-ribbon-removing");
action.addEventListener("transitionend", async () => {
action.addEventListener("transitionend", () => {
action.remove();
this.actions.delete(pair);
});

View file

@ -45,6 +45,22 @@
margin-bottom: -18px;
}
.cmdr-menu-item {
display: flex;
}
.cmdr-slider .clickable-icon.is-disabled {
opacity: 0.35;
cursor: default;
pointer-events: none;
}
.cmdr-advanced-toolbar-settings .extra-setting-button.is-disabled {
opacity: 0.35;
cursor: default;
pointer-events: none;
}
.cmdr-menu-more-options {
color: var(--text-muted);
position: absolute;
@ -52,11 +68,37 @@
padding-top: 2px;
transform: scale(0.9);
transition: all 150ms ease;
display: none;
&.is-visible {
display: block;
}
&:hover {
color: var(--text-primary);
}
}
.cmdr-confirm-delete-modal {
z-index: 99;
}
.cmdr-hide-suggestions {
display: none !important;
}
.cmdr-name-input-wrapper-parent {
display: block !important;
}
.cmdr-confetti-canvas {
position: fixed;
width: 100vw;
height: 100vh;
top: 0;
left: 0;
pointer-events: none;
z-index: 100;
}
.cmdr-mobile .cmdr-credits {
place-content: center;
}

View file

@ -1,13 +1,12 @@
import { h } from "preact";
import { Command, PluginManifest } from "obsidian";
/* eslint-disable no-unused-vars */
export enum Action {
COMMAND,
DELAY,
EDITOR,
LOOP,
}
/* eslint-enable no-unused-vars */
export type MacroItem =
| { action: Action.COMMAND; commandId: string }
@ -63,7 +62,7 @@ export interface Tab {
tab: h.JSX.Element;
}
export type Mode = "desktop" | "any" | "mobile" | string;
export type Mode = "desktop" | "any" | "mobile" | (string & {});
export interface CommandIconPair {
id: string;
@ -73,7 +72,6 @@ export interface CommandIconPair {
color?: string;
}
/* eslint-disable no-unused-vars */
declare module "obsidian" {
interface MenuItem {
dom: HTMLElement;
@ -85,6 +83,7 @@ declare module "obsidian" {
[id: string]: Command;
};
executeCommandById: (id: string) => void;
removeCommand: (id: string) => void;
};
plugins: {
manifests: {
@ -111,6 +110,11 @@ declare module "obsidian" {
title: string;
callback: () => void;
}[];
items: {
icon: string;
title: string;
buttonEl: HTMLElement;
}[];
collapseButtonEl: HTMLElement;
ribbonItemsEl: HTMLElement;
addRibbonItemButton: (
@ -128,4 +132,8 @@ declare module "obsidian" {
interface WorkspaceLeaf {
containerEl: HTMLElement;
}
interface Vault {
getConfig(key: string): unknown;
}
}

View file

@ -34,7 +34,7 @@ export default class AddCommandModal extends FuzzySuggestModal<Command> {
this.onChooseItem = (item): void => resolve(item);
//This is wrapped inside a setTimeout, because onClose is called before onChooseItem
this.onClose = (): number =>
window.setTimeout(() => reject("No Command selected"), 0);
window.setTimeout(() => reject(new Error("No Command selected")), 0);
});
}
@ -62,6 +62,6 @@ export default class AddCommandModal extends FuzzySuggestModal<Command> {
}
// This will be overriden anyway, but typescript complains if it's not declared
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-empty-function
// eslint-disable-next-line @typescript-eslint/no-empty-function -- overridden by awaitSelection, declared only to satisfy the type
public onChooseItem(item: Command, evt: MouseEvent | KeyboardEvent): void {}
}

View file

@ -4,14 +4,12 @@ import CommanderPlugin from "src/main";
export default class ChooseCustomNameModal extends SuggestModal<string> {
public constructor(
// eslint-disable-next-line no-unused-vars
private defaultName: string,
// eslint-disable-next-line no-unused-vars
private plugin: CommanderPlugin
) {
super(plugin.app);
this.setPlaceholder(t("Use a custom name"));
this.resultContainerEl.style.display = "none";
this.resultContainerEl.addClass("cmdr-hide-suggestions");
this.setInstructions([
{
@ -30,13 +28,13 @@ export default class ChooseCustomNameModal extends SuggestModal<string> {
}
public onOpen(): void {
super.onOpen();
void super.onOpen();
this.inputEl.value = this.defaultName;
const wrapper = createDiv({ cls: "cmdr-name-input-wrapper" });
this.inputEl.parentNode?.insertBefore(wrapper, this.inputEl);
wrapper.appendChild(this.inputEl);
wrapper.parentElement!.style.display = "block";
wrapper.parentElement!.addClass("cmdr-name-input-wrapper-parent");
const btn = createEl("button", { text: t("Save"), cls: "mod-cta" });
btn.onclick = (e): void => this.selectSuggestion(this.inputEl.value, e);
@ -49,7 +47,7 @@ export default class ChooseCustomNameModal extends SuggestModal<string> {
this.onChooseSuggestion = (item): void => resolve(item);
//This is wrapped inside a setTimeout, because onClose is called before onChooseItem
this.onClose = (): number =>
window.setTimeout(() => reject("No Name selected"), 0);
window.setTimeout(() => reject(new Error("No Name selected")), 0);
});
}
@ -58,14 +56,14 @@ export default class ChooseCustomNameModal extends SuggestModal<string> {
}
// This isn't needed, since we just want a text field without options
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-empty-function
// eslint-disable-next-line @typescript-eslint/no-empty-function -- intentionally a no-op, only a text field is needed
public renderSuggestion(value: string, el: HTMLElement): void {}
// This will be overriden anyway, but typescript complains if it's not declared
/* eslint-disable no-unused-vars, @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/no-empty-function -- overridden by awaitSelection, declared only to satisfy the type */
public onChooseSuggestion(
item: string,
evt: MouseEvent | KeyboardEvent
): void {}
/* eslint-enable no-unused-vars, @typescript-eslint/no-empty-function */
/* eslint-enable @typescript-eslint/no-empty-function -- overridden by awaitSelection, declared only to satisfy the type */
}

View file

@ -33,7 +33,7 @@ export default class ChooseIconModal extends FuzzySuggestModal<string> {
this.onChooseItem = (item): void => resolve(item);
//This is wrapped inside a setTimeout, because onClose is called before onChooseItem
this.onClose = (): number =>
window.setTimeout(() => reject("No Icon selected"), 0);
window.setTimeout(() => reject(new Error("No Icon selected")), 0);
});
}
@ -62,6 +62,6 @@ export default class ChooseIconModal extends FuzzySuggestModal<string> {
}
// This will be overriden anyway, but typescript complains if it's not declared
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-empty-function
// eslint-disable-next-line @typescript-eslint/no-empty-function -- overridden by awaitSelection, declared only to satisfy the type
public onChooseItem(_: string, __: MouseEvent | KeyboardEvent): void {}
}

View file

@ -14,8 +14,8 @@ export default function About({
<button
className="mod-cta"
onClick={(e): void => {
showConfetti(e);
setTimeout(
void showConfetti(e);
window.setTimeout(
() =>
location.replace("https://forms.gle/hPjn61G9bqqFb3256"),
Math.random() * 800 + 500
@ -30,8 +30,8 @@ export default function About({
<button
className="mod-cta"
onClick={(e): void => {
showConfetti(e);
setTimeout(
void showConfetti(e);
window.setTimeout(
() => location.replace("https://ko-fi.com/phibr0"),
Math.random() * 800 + 500
);

View file

@ -1,34 +1,82 @@
import { Notice, Platform, setIcon, Setting } from "obsidian";
import {
ExtraButtonComponent,
Notice,
Platform,
setIcon,
Setting,
SliderComponent,
} from "obsidian";
import { Fragment, h } from "preact";
import { useEffect, useRef } from "preact/hooks";
import { DEFAULT_SETTINGS } from "src/constants";
import t from "src/l10n";
import CommanderPlugin from "src/main";
import { injectIcons, ObsidianIcon, updateStyles } from "src/util";
import ChooseIconModal from "../chooseIconModal";
function addResettableSlider(
containerEl: HTMLElement,
opts: {
name: string;
desc: string;
min: number;
max: number;
step: number;
value: number;
defaultValue: number;
onChange: (value: number) => Promise<void>;
}
): void {
let resetBtn: ExtraButtonComponent;
let slider: SliderComponent;
new Setting(containerEl)
.setName(opts.name)
.setDesc(opts.desc)
.addSlider((cb) => {
slider = cb;
cb.setLimits(opts.min, opts.max, opts.step)
.setValue(opts.value)
.setDynamicTooltip()
.onChange(async (value) => {
await opts.onChange(value);
resetBtn.setDisabled(value === opts.defaultValue);
});
})
.addExtraButton((bt) => {
resetBtn = bt;
bt.setIcon("reset")
.setTooltip(t("Restore default"))
.setDisabled(opts.value === opts.defaultValue)
.onClick(async () => {
slider.setValue(opts.defaultValue);
await opts.onChange(opts.defaultValue);
resetBtn.setDisabled(true);
});
});
}
function render(containerEl: HTMLElement, plugin: CommanderPlugin): void {
containerEl.empty();
new Setting(containerEl)
.setName("Toolbar Row Count")
.setDesc(
"Set how many Rows the Mobile Toolbar should have. Set this to 0 to remove the Toolbar."
)
.addSlider((cb) =>
cb
.setLimits(0, 5, 1)
.setValue(plugin.settings.advancedToolbar.rowCount)
.setDynamicTooltip()
.onChange(async (value) => {
plugin.settings.advancedToolbar.rowCount = value;
await plugin.saveSettings();
updateStyles(plugin.settings.advancedToolbar);
})
);
addResettableSlider(containerEl, {
name: "Toolbar row count",
desc: "Set how many rows the mobile toolbar should have. Set this to 0 to remove the toolbar.",
min: 0,
max: 5,
step: 1,
value: plugin.settings.advancedToolbar.rowCount,
defaultValue: DEFAULT_SETTINGS.advancedToolbar.rowCount,
onChange: async (value) => {
plugin.settings.advancedToolbar.rowCount = value;
await plugin.saveSettings();
updateStyles(plugin.settings.advancedToolbar);
},
});
new Setting(containerEl)
.setName("Column Layout")
.setName("Column layout")
.setDesc(
"Use a column based layout instead of the default row. This makes it easier to arrange the Commands."
"Use a column based layout instead of the default row. This makes it easier to arrange the commands."
)
.addToggle((cb) =>
cb
@ -51,26 +99,24 @@ function render(containerEl: HTMLElement, plugin: CommanderPlugin): void {
// })
// })
new Setting(containerEl)
.setName("Bottom Offset")
.setDesc(
"Offset the Toolbar from the Bottom of the Screen. This is useful if the toolbar is partially obscured by other UI Elements."
)
.addSlider((cb) =>
cb
.setLimits(0, 32, 1)
.setValue(plugin.settings.advancedToolbar.heightOffset)
.setDynamicTooltip()
.onChange(async (value) => {
plugin.settings.advancedToolbar.heightOffset = value;
await plugin.saveSettings();
updateStyles(plugin.settings.advancedToolbar);
})
);
addResettableSlider(containerEl, {
name: "Bottom offset",
desc: "Offset the toolbar from the bottom of the screen. This is useful if the toolbar is partially obscured by other UI elements.",
min: 0,
max: 32,
step: 1,
value: plugin.settings.advancedToolbar.heightOffset,
defaultValue: DEFAULT_SETTINGS.advancedToolbar.heightOffset,
onChange: async (value) => {
plugin.settings.advancedToolbar.heightOffset = value;
await plugin.saveSettings();
updateStyles(plugin.settings.advancedToolbar);
},
});
if (Platform.isMobile) {
const description = document.createDocumentFragment();
description.appendChild(createEl("h3", { text: "Custom Icons" }));
const description = createFragment();
description.appendChild(createEl("h3", { text: "Custom icons" }));
containerEl.appendChild(description);
plugin.getCommandsWithoutIcons().forEach((command) => {
@ -90,7 +136,7 @@ function render(containerEl: HTMLElement, plugin: CommanderPlugin): void {
)?.iconID;
currentIcon
? setIcon(iconDiv, currentIcon)
: bt.setButtonText("No Icon");
: bt.setButtonText("No icon");
}
bt.onClick(async () => {
const icon = await new ChooseIconModal(
@ -115,7 +161,7 @@ function render(containerEl: HTMLElement, plugin: CommanderPlugin): void {
})
.addExtraButton((bt) => {
bt.setIcon("reset")
.setTooltip("Reset to default - Requires a restart")
.setTooltip("Reset to default - requires a restart")
.onClick(async () => {
plugin.settings.advancedToolbar.mappedIcons =
plugin.settings.advancedToolbar.mappedIcons.filter(
@ -126,7 +172,7 @@ function render(containerEl: HTMLElement, plugin: CommanderPlugin): void {
await plugin.saveSettings();
render(containerEl, plugin);
new Notice(
"If the default Icon doesn't appear, you might have to restart Obsidian."
"If the default icon doesn't appear, you might have to restart Obsidian."
);
});
});
@ -134,22 +180,22 @@ function render(containerEl: HTMLElement, plugin: CommanderPlugin): void {
}
const advancedEl = containerEl.appendChild(
createEl("div", {
createDiv({
cls: "cmdr-sep-con",
attr: { style: "margin-top: 64px" },
})
);
advancedEl.appendChild(
createEl("div", {
text: "Advanced Settings",
createDiv({
text: "Advanced settings",
attr: { style: "margin-bottom: 8px; font-weight: bold" },
})
);
new Setting(advancedEl)
.setName("Button Height")
.setName("Button height")
.setDesc(
"Change the Height of each Button inside the Mobile Toolbar (in px)."
"Change the height of each button inside the mobile toolbar (in px)."
)
.addText((cb) =>
cb
@ -170,9 +216,9 @@ function render(containerEl: HTMLElement, plugin: CommanderPlugin): void {
})
);
new Setting(advancedEl)
.setName("Button Width")
.setName("Button width")
.setDesc(
"Change the Width of each Button inside the Mobile Toolbar (in px)."
"Change the width of each button inside the mobile toolbar (in px)."
)
.addText((cb) =>
cb
@ -192,22 +238,20 @@ function render(containerEl: HTMLElement, plugin: CommanderPlugin): void {
}
})
);
new Setting(advancedEl)
.setName("Toolbar Extra Spacing")
.setDesc(
"Some Themes need extra spacing in the toolbar. If your Toolbar doesn't wrap properly, try increasing this value."
)
.addSlider((cb) =>
cb
.setLimits(0, 64, 1)
.setValue(plugin.settings.advancedToolbar.spacing)
.setDynamicTooltip()
.onChange(async (value) => {
plugin.settings.advancedToolbar.spacing = value;
await plugin.saveSettings();
updateStyles(plugin.settings.advancedToolbar);
})
);
addResettableSlider(advancedEl, {
name: "Toolbar extra spacing",
desc: "Some themes need extra spacing in the toolbar. If your toolbar doesn't wrap properly, try increasing this value.",
min: 0,
max: 64,
step: 1,
value: plugin.settings.advancedToolbar.spacing,
defaultValue: DEFAULT_SETTINGS.advancedToolbar.spacing,
onChange: async (value) => {
plugin.settings.advancedToolbar.spacing = value;
await plugin.saveSettings();
updateStyles(plugin.settings.advancedToolbar);
},
});
}
export default function AdvancedToolbarSettings({
@ -221,7 +265,9 @@ export default function AdvancedToolbarSettings({
if (ref.current) {
render(ref.current, plugin);
}
return () => ref.current && ref.current.empty();
return (): void => {
ref.current && ref.current.empty();
};
}, []);
return (
@ -251,7 +297,11 @@ export default function AdvancedToolbarSettings({
</button>
)}
</div>
<div ref={ref} style={{ paddingBottom: "128px" }} />
<div
ref={ref}
className="cmdr-advanced-toolbar-settings"
style={{ paddingBottom: "128px" }}
/>
</Fragment>
);
}

View file

@ -4,7 +4,6 @@ import { Ref, useEffect, useRef, useState } from "preact/hooks";
interface Props {
value: string;
// eslint-disable-next-line no-unused-vars
handleChange: (e: h.JSX.TargetedKeyboardEvent<HTMLInputElement>) => void;
ariaLabel: string;
}
@ -46,8 +45,7 @@ export default function ChangeableText({
) : (
<span
onDblClick={({ target }): void => {
/* @ts-ignore */
setWidth(target?.offsetWidth);
setWidth((target as HTMLElement | null)?.offsetWidth ?? 0);
setShowInput(true);
}}
aria-label={ariaLabel}

View file

@ -4,7 +4,6 @@ import { useEffect, useRef } from "preact/hooks";
interface ColorPickerProps {
initialColor: string;
// eslint-disable-next-line no-unused-vars
onChange: (_color: string) => void;
}
@ -20,7 +19,9 @@ export const ColorPicker = ({
.onChange(onChange);
}
return () => ref.current?.empty?.();
return (): void => {
ref.current?.empty?.();
};
}, [onChange, initialColor]);
return <div ref={ref} className="cmdr-flex cmdr-items-center" />;

View file

@ -10,7 +10,6 @@ import { SliderComponent } from "./settingComponent";
interface MacroBuilderProps {
plugin: CommanderPlugin;
macro: Macro;
// eslint-disable-next-line no-unused-vars
onSave: (macro: Macro) => void;
onCancel: () => void;
}
@ -24,7 +23,7 @@ export default function ({
const [icon, setIcon] = useState(macro.icon || "star");
const [startup, setStartup] = useState(macro.startup || false);
const [macroCommands, setMacroCommands] = useState<MacroItem[]>(
JSON.parse(JSON.stringify(macro.macro)) || []
(JSON.parse(JSON.stringify(macro.macro)) as MacroItem[]) || []
);
const handleAddCommand = async (): Promise<void> => {
@ -238,8 +237,9 @@ export default function ({
id="checkbox"
checked={startup}
onChange={({ target }): void => {
//@ts-expect-error
setStartup(target?.checked ?? false);
setStartup(
(target as HTMLInputElement | null)?.checked ?? false
);
}}
/>
<label htmlFor="checkbox">Auto-Run on Startup</label>

View file

@ -7,14 +7,11 @@ import MacroBuilderComponent from "./MacroBuilder";
export default class MacroBuilderModal extends Modal {
private plugin: CommanderPlugin;
private macro: Macro;
// eslint-disable-next-line no-unused-vars
private onSave: (_macro: Macro) => void;
public constructor(
plugin: CommanderPlugin,
// eslint-disable-next-line no-unused-vars
macro: Macro,
// eslint-disable-next-line no-unused-vars
onSave: (_macro: Macro) => void
) {
super(plugin.app);
@ -24,13 +21,13 @@ export default class MacroBuilderModal extends Modal {
}
public onOpen(): void {
this.titleEl.setText("Macro Builder");
this.titleEl.setText("Macro builder");
render(
h(MacroBuilderComponent, {
plugin: this.plugin,
macro: this.macro,
onSave: this.onSave,
onCancel: this.close.bind(this),
onCancel: (): void => this.close(),
}),
this.contentEl
);

View file

@ -1,5 +1,6 @@
import { Platform } from "obsidian";
import { Fragment, h } from "preact";
import { useState } from "preact/hooks";
import t from "src/l10n";
import CommanderPlugin from "src/main";
import { Macro } from "src/types";
@ -16,6 +17,9 @@ export default function MacroViewer({
plugin,
macros,
}: MacroBuilderProps): h.JSX.Element {
const [, setUpdateTick] = useState(0);
const forceUpdate = (): void => setUpdateTick((tick) => tick + 1);
const handleBuilder = (macro: Macro, idx?: number): void => {
const onClose = (updatedMacro: Macro): void => {
macros.splice(
@ -24,8 +28,8 @@ export default function MacroViewer({
updatedMacro
);
plugin.saveSettings();
this.forceUpdate();
void plugin.saveSettings();
forceUpdate();
updateMacroCommands(plugin);
modal.close();
};
@ -35,8 +39,8 @@ export default function MacroViewer({
const handleDelete = (idx: number): void => {
macros.splice(idx, 1);
plugin.saveSettings();
this.forceUpdate();
void plugin.saveSettings();
forceUpdate();
updateMacroCommands(plugin);
};

View file

@ -15,11 +15,8 @@ interface CommandViewerProps {
handleUp: () => void;
handleDown: () => void;
handleNewIcon: () => void;
// eslint-disable-next-line no-unused-vars
handleRename: (_name: string) => void;
// eslint-disable-next-line no-unused-vars
handleModeChange: (_mode?: string) => void;
// eslint-disable-next-line no-unused-vars
handleColorChange: (_color?: string) => void;
sortable?: boolean;
}
@ -100,12 +97,11 @@ export default function CommandComponent({
);
}
const owningPluginID = cmd.id.split(":").first();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const owningPlugin = plugin.app.plugins.manifests[owningPluginID!];
const isInternal = !owningPlugin;
const isChecked =
cmd.hasOwnProperty("checkCallback") ||
cmd.hasOwnProperty("editorCheckCallback");
(Object.prototype.hasOwnProperty.call(cmd, "checkCallback") as boolean) ||
(Object.prototype.hasOwnProperty.call(cmd, "editorCheckCallback") as boolean);
const modeIcon = getModeIcon(pair.mode);
const modeName = pair.mode.match(/desktop|mobile|any/)
@ -128,8 +124,9 @@ export default function CommandComponent({
<ChangeableText
ariaLabel={t("Double click to rename")}
handleChange={({ target }): void => {
/* @ts-ignore */
handleRename(target?.value);
handleRename(
(target as HTMLInputElement | null)?.value ?? ""
);
}}
value={pair.name}
/>

View file

@ -1,4 +1,5 @@
import { createContext, Fragment, h } from "preact";
import { useState } from "preact/hooks";
import CommanderPlugin from "src/main";
import CommandComponent from "./commandComponent";
import CommandManagerBase from "src/manager/commands/commandManager";
@ -10,7 +11,6 @@ import t from "src/l10n";
import { Platform } from "obsidian";
import Logo from "./Logo";
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
export const ManagerContext = createContext<CommandManagerBase>(null!);
interface CommandViewerProps {
@ -25,6 +25,9 @@ export default function CommandViewer({
children,
sortable = true,
}: CommandViewerProps): h.JSX.Element {
const [, setUpdateTick] = useState(0);
const forceUpdate = (): void => setUpdateTick((tick) => tick + 1);
return (
<Fragment>
<ManagerContext.Provider value={manager}>
@ -48,7 +51,7 @@ export default function CommandViewer({
).didChooseRemove())
) {
await manager.removeCommand(cmd);
this.forceUpdate();
forceUpdate();
}
}}
handleUp={(): void => {
@ -57,8 +60,8 @@ export default function CommandViewer({
idx,
idx - 1
);
manager.reorder();
this.forceUpdate();
void manager.reorder();
forceUpdate();
}}
handleDown={(): void => {
arrayMoveMutable(
@ -66,16 +69,16 @@ export default function CommandViewer({
idx,
idx + 1
);
manager.reorder();
this.forceUpdate();
void manager.reorder();
forceUpdate();
}}
handleRename={async (
name
): Promise<void> => {
cmd.name = name;
await plugin.saveSettings();
manager.reorder();
this.forceUpdate();
await manager.reorder();
forceUpdate();
}}
handleNewIcon={async (): Promise<void> => {
const newIcon =
@ -85,8 +88,8 @@ export default function CommandViewer({
if (newIcon && newIcon !== cmd.icon) {
cmd.icon = newIcon;
await plugin.saveSettings();
manager.reorder();
this.forceUpdate();
await manager.reorder();
forceUpdate();
}
dispatchEvent(
new Event("cmdr-icon-changed")
@ -110,15 +113,16 @@ export default function CommandViewer({
cmd.mode =
mode || modes[currentIdx + 1];
await plugin.saveSettings();
manager.reorder();
this.forceUpdate();
await manager.reorder();
forceUpdate();
}}
handleColorChange={async (
color?: string
): Promise<void> => {
cmd.color = color;
await plugin.saveSettings();
manager.reorder();
await manager.reorder();
forceUpdate();
}}
/>
);
@ -147,8 +151,8 @@ export default function CommandViewer({
onClick={async (): Promise<void> => {
const pair = await chooseNewCommand(plugin);
await manager.addCommand(pair);
manager.reorder();
this.forceUpdate();
await manager.reorder();
forceUpdate();
}}
>
{t("Add command")}

View file

@ -15,7 +15,7 @@ export function confirmDeleteComponent({
className="mod-warning"
onClick={async (): Promise<void> => {
modal.plugin.settings.confirmDeletion = false;
modal.plugin.saveSettings();
await modal.plugin.saveSettings();
modal.remove = true;
modal.close();

View file

@ -18,8 +18,7 @@ export function LeftRibbonHider({
const hiddenCommands = plugin.settings.hide.leftRibbon;
useEffect(() => {
setRibbonCommands(
// @ts-expect-error
app.workspace.leftRibbon.items.map((item) => ({
plugin.app.workspace.leftRibbon.items.map((item) => ({
name: item.title,
icon: item.icon,
}))

View file

@ -1,5 +1,5 @@
import { h } from "preact";
import { useEffect } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
import t from "src/l10n";
import { ObsidianIcon } from "src/util";
import MobileModifyModal from "../mobileModifyModal";
@ -7,18 +7,20 @@ import { ColorPicker } from "./ColorPicker";
import CommanderPlugin from "src/main";
export default function MobileModifyComponent({
plugin,
plugin,
modal: controller,
}: {
plugin: CommanderPlugin;
modal: MobileModifyModal;
}): h.JSX.Element {
const [, setUpdateTick] = useState(0);
useEffect(() => {
const update = (): void => {
this.forceUpdate();
setUpdateTick((tick) => tick + 1);
};
addEventListener("cmdr-icon-changed", update);
return () => removeEventListener("cmdr-icon-changed", update);
return (): void => removeEventListener("cmdr-icon-changed", update);
}, []);
return (

View file

@ -30,7 +30,6 @@ function BaseComponent({
interface SettingProps<T> {
name: string;
description: string;
// eslint-disable-next-line no-unused-vars
changeHandler: (value: T) => void;
value: T;
min?: number;
@ -91,7 +90,13 @@ export function EyeToggleComponent({
);
}
export function SliderComponent(props: SettingProps<number>): h.JSX.Element {
interface SliderProps extends SettingProps<number> {
defaultValue?: number;
}
export function SliderComponent({
defaultValue,
...props
}: SliderProps): h.JSX.Element {
const [val, setVal] = useState(props.value);
return (
@ -100,7 +105,26 @@ export function SliderComponent(props: SettingProps<number>): h.JSX.Element {
name={props.name}
className="cmdr-slider"
>
<div>
<div class="cmdr-flex cmdr-items-center">
{defaultValue !== undefined && (
<ObsidianIcon
aria-label={t("Restore default")}
icon="reset"
size={16}
className={`clickable-icon${
val === defaultValue ? " is-disabled" : ""
}`}
aria-disabled={val === defaultValue}
onClick={
val === defaultValue
? undefined
: (): void => {
setVal(defaultValue);
props.changeHandler(defaultValue);
}
}
/>
)}
<ChangeableText
ariaLabel={t("Double click to enter custom value")}
value={val.toString()}
@ -121,12 +145,10 @@ export function SliderComponent(props: SettingProps<number>): h.JSX.Element {
step={props.step ?? "1"}
value={val}
onPointerMove={({ target }): void => {
{/*@ts-expect-error*/}
if (val !== target.value) {
{/*@ts-expect-error*/}
setVal(target.value);
{/*@ts-expect-error*/}
props.changeHandler(target.value);
const value = Number((target as HTMLInputElement).value);
if (!isNaN(value) && val !== value) {
setVal(value);
props.changeHandler(value);
}
}}
/>

View file

@ -1,6 +1,7 @@
import { Platform } from "obsidian";
import { Fragment, h } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { DEFAULT_SETTINGS } from "src/constants";
import t from "src/l10n";
import { Tab } from "src/types";
import { ObsidianIcon, updateSpacing } from "src/util";
@ -37,18 +38,16 @@ export default function settingTabComponent({
useEffect(() => {
addEventListener("keydown", tabToNextTab);
return () => removeEventListener("keydown", tabToNextTab);
return (): void => removeEventListener("keydown", tabToNextTab);
}, [activeTab]);
//This is used to remove the initial onclick event listener.
if (Platform.isMobile) {
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const old_element = document.querySelector(
".modal-setting-back-button"
)!;
const new_element = old_element.cloneNode(true);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
old_element.parentNode!.replaceChild(new_element, old_element);
setOpen(true);
}, []);
@ -61,11 +60,9 @@ export default function settingTabComponent({
if (!el) return;
if (!open) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
el.parentElement!.lastChild!.textContent = tabs[activeTab].name;
el.onclick = (): void => setOpen(true);
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
el.parentElement!.lastChild!.textContent = "Commander";
el.onclick = (): void => plugin.app.setting.closeActiveTab();
}
@ -96,12 +93,13 @@ export default function settingTabComponent({
}
changeHandler={async (value): Promise<void> => {
plugin.settings.showAddCommand = !value;
plugin.manager.pageHeader.reorder();
await plugin.manager.pageHeader.reorder();
await plugin.saveSettings();
}}
/>
<SliderComponent
value={plugin.settings.spacing}
defaultValue={DEFAULT_SETTINGS.spacing}
name={t(
"Choose custom spacing for Command Buttons"
)}
@ -172,7 +170,7 @@ export default function settingTabComponent({
<button
onClick={(): void => {
plugin.app.setting.openTabById("appearance");
setTimeout(() => {
window.setTimeout(() => {
plugin.app.setting.activeTab.containerEl.scroll(
{
behavior: "smooth",
@ -180,14 +178,14 @@ export default function settingTabComponent({
}
);
plugin.app.setting.activeTab.containerEl
.querySelectorAll(
".setting-item-heading"
)[1]
// @ts-ignore
.nextSibling?.nextSibling?.nextSibling?.addClass?.(
"cmdr-cta"
);
(
plugin.app.setting.activeTab.containerEl
.querySelectorAll(
".setting-item-heading"
)[1]
.nextSibling?.nextSibling
?.nextSibling as HTMLElement | null
)?.addClass?.("cmdr-cta");
}, 50);
}}
className="mod-cta"
@ -323,9 +321,7 @@ export default function settingTabComponent({
interface TabHeaderProps {
tabs: Tab[];
activeTab: number;
// eslint-disable-next-line no-unused-vars
setActiveTab: (idx: number) => void;
// eslint-disable-next-line no-unused-vars
setOpen: (open: boolean) => void;
}
export function TabHeader({
@ -348,7 +344,7 @@ export function TabHeader({
}
el.addEventListener("wheel", handleScroll);
return () => el.removeEventListener("wheel", handleScroll);
return (): void => el.removeEventListener("wheel", handleScroll);
}, []);
useEffect(

View file

@ -1,23 +1,20 @@
import { Modal } from "obsidian";
import { h, render, VNode } from "preact";
import { h, render } from "preact";
import t from "src/l10n";
import CommanderPlugin from "src/main";
import { confirmDeleteComponent } from "./components/confirmDeleteComponent";
export default class ConfirmDeleteModal extends Modal {
private reactComponent: VNode;
public remove: boolean;
// eslint-disable-next-line no-unused-vars
public constructor(public plugin: CommanderPlugin) {
super(plugin.app);
}
public async onOpen(): Promise<void> {
this.titleEl.innerText = t("Remove Command");
this.containerEl.style.zIndex = "99";
this.reactComponent = h(confirmDeleteComponent, { modal: this });
render(this.reactComponent, this.contentEl);
this.containerEl.addClass("cmdr-confirm-delete-modal");
render(h(confirmDeleteComponent, { modal: this }), this.contentEl);
}
public async didChooseRemove(): Promise<boolean> {

View file

@ -1,12 +1,10 @@
/* eslint-disable no-unused-vars */
import { CommandIconPair } from "src/types";
import { Modal } from "obsidian";
import { h, render, VNode } from "preact";
import { h, render } from "preact";
import MobileModifyComponent from "./components/mobileModifyComponent";
import CommanderPlugin from "src/main";
export default class MobileModifyModal extends Modal {
private reactComponent: VNode;
public remove: boolean;
public constructor(
@ -22,11 +20,13 @@ export default class MobileModifyModal extends Modal {
public async onOpen(): Promise<void> {
this.titleEl.innerText = this.pair.name;
this.reactComponent = h(MobileModifyComponent, {
plugin: this.plugin,
modal: this,
});
render(this.reactComponent, this.contentEl);
render(
h(MobileModifyComponent, {
plugin: this.plugin,
modal: this,
}),
this.contentEl
);
}
public onClose(): void {

View file

@ -24,7 +24,7 @@ export async function chooseNewCommand(
const command = await new AddCommandModal(plugin).awaitSelection();
let icon;
if (!command.hasOwnProperty("icon")) {
if (!(Object.prototype.hasOwnProperty.call(command, "icon") as boolean)) {
icon = await new ChooseIconModal(plugin).awaitSelection();
}
@ -36,7 +36,6 @@ export async function chooseNewCommand(
return {
id: command.id,
//This cannot be undefined anymore
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
icon: icon ?? command.icon!,
name: name || command.name,
mode: "any",
@ -59,7 +58,6 @@ export function ObsidianIcon({
const iconEl = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
setIcon(iconEl.current!, icon);
}, [icon, size]);
@ -99,17 +97,10 @@ export function updateHiderStylesheet(settings: CommanderSettings): void {
}
export async function showConfetti({ target }: MouseEvent): Promise<void> {
const myCanvas = activeDocument.createElement("canvas");
const myCanvas = createEl("canvas", {
cls: "cmdr-confetti-canvas",
});
activeDocument.body.appendChild(myCanvas);
myCanvas.style.position = "fixed";
myCanvas.style.width = "100vw";
myCanvas.style.height = "100vh";
myCanvas.style.top = "0px";
myCanvas.style.left = "0px";
//@ts-ignore
myCanvas.style["pointer-events"] = "none";
//@ts-ignore
myCanvas.style["z-index"] = "100";
const myConfetti = confetti.create(myCanvas, {
resize: true,
@ -143,7 +134,6 @@ export function updateMacroCommands(plugin: CommanderPlugin): void {
p.startsWith("cmdr:macro-")
);
for (const command of oldCommands) {
//@ts-ignore
plugin.app.commands.removeCommand(command);
}
@ -154,7 +144,7 @@ export function updateMacroCommands(plugin: CommanderPlugin): void {
name: macro.name,
icon: macro.icon,
callback: () => {
plugin.executeMacro(Number(idx));
void plugin.executeMacro(Number(idx));
},
});
}

29
vitest.config.ts Normal file
View file

@ -0,0 +1,29 @@
import { defineConfig } from "vitest/config";
import path from "path";
import { fileURLToPath } from "url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
test: {
environment: "jsdom",
globals: false,
setupFiles: ["src/__tests__/setup.ts"],
},
resolve: {
alias: [
{
find: "obsidian",
replacement: path.resolve(__dirname, "src/__tests__/__mocks__/obsidian.ts"),
},
{
find: /.*\/l10n$/,
replacement: path.resolve(__dirname, "src/__tests__/__mocks__/l10n.ts"),
},
{
find: "src",
replacement: path.resolve(__dirname, "src"),
},
],
},
});