jsmorabito_obsidian-commander/src/ui/components/commandComponent.tsx

284 lines
7.2 KiB
TypeScript
Raw Normal View History

import { Notice, Platform } from "obsidian";
2022-06-06 16:29:14 +00:00
import { Fragment, h } from "preact";
2022-07-11 22:07:28 +00:00
import t from "src/l10n";
2022-06-06 16:29:14 +00:00
import { CommandIconPair } from "src/types";
2022-07-11 22:07:28 +00:00
import { getCommandFromId, ObsidianIcon } from "src/util";
import MobileModifyModal from "../mobileModifyModal";
import ChangeableText from "./ChangeableText";
import { ColorPicker } from "./ColorPicker";
2024-10-11 01:16:41 +00:00
import CommanderPlugin from "src/main";
2022-06-06 16:29:14 +00:00
interface CommandViewerProps {
2024-10-11 01:16:41 +00:00
plugin: CommanderPlugin;
2022-06-06 16:29:14 +00:00
pair: CommandIconPair;
handleRemove: () => void;
handleUp: () => void;
handleDown: () => void;
handleNewIcon: () => void;
handleRename: (_name: string) => void;
handleModeChange: (_mode?: string) => void;
handleColorChange: (_color?: string) => void;
sortable?: boolean;
2022-06-06 16:29:14 +00:00
}
2022-07-11 22:07:28 +00:00
export default function CommandComponent({
2024-10-11 01:16:41 +00:00
plugin,
2022-07-11 22:07:28 +00:00
pair,
handleRemove,
handleDown,
handleUp,
handleNewIcon,
handleRename,
handleModeChange,
handleColorChange,
sortable = true,
2022-07-11 22:07:28 +00:00
}: CommandViewerProps): h.JSX.Element {
2024-10-11 01:16:41 +00:00
const cmd = getCommandFromId(pair.id, plugin);
2022-06-06 16:29:14 +00:00
if (!cmd) {
return (
<Fragment>
{Platform.isDesktop && (
<div className="setting-item mod-toggle">
<ObsidianIcon
icon="alert-triangle"
size={20}
className="cmdr-icon clickable-icon mod-warning"
/>
<div className="setting-item-info">
<div className="setting-item-name">{pair.name}</div>
<div className="setting-item-description">
{t(
"This Command is not available on this device."
)}
</div>
</div>
<div className="setting-item-control">
<button
className="mod-warning"
style="display: flex"
onClick={handleRemove}
aria-label={t("Delete")}
>
<ObsidianIcon icon="lucide-trash" />
</button>
</div>
</div>
)}
{Platform.isMobile && (
<div
className="mobile-option-setting-item"
onClick={(): void => {
new Notice(
t(
"This Command is not available on this device."
)
);
}}
>
<span
className="mobile-option-setting-item-remove-icon"
onClick={handleRemove}
>
<ObsidianIcon
icon="minus-with-circle"
size={22}
style={{ color: "var(--text-error)" }}
/>
</span>
<span className="mobile-option-setting-item-option-icon mod-warning">
<ObsidianIcon icon={"alert-triangle"} size={22} />
</span>
<span className="mobile-option-setting-item-name">
{pair.name}
</span>
</div>
)}
</Fragment>
);
2022-06-06 16:29:14 +00:00
}
const owningPluginID = cmd.id.split(":").first();
2024-10-11 01:16:41 +00:00
const owningPlugin = plugin.app.plugins.manifests[owningPluginID!];
2022-06-06 16:29:14 +00:00
const isInternal = !owningPlugin;
2022-07-11 22:07:28 +00:00
const isChecked =
lint: replace remaining any-typed casts with proper types Resolves the rest of the no-unsafe-* family by giving TypeScript enough information instead of suppressing it: - Extend the Obsidian module augmentation (types.ts) with App.commands.removeCommand and Vault.getConfig so util.tsx and main.ts no longer need @ts-ignore around them. - Cast event.target to the concrete HTMLElement subtype at each DOM event handler that reads .value/.checked/.offsetWidth (ChangeableText, MacroBuilder, commandComponent) instead of @ts-ignore/@ts-expect-error. - Cast JSON.parse() results to their known shape (MacroBuilder's macro clone, locales.test.ts's loadJson, main.ts's loadSettings) instead of letting `any` flow through. - Drop menuManager's unusual `this: (_item: MenuItem) => void` parameter and the .call(this, ...) it required - the returned closure never reads `this`, so this was both unnecessary and, under this project's non-strict bind/call/apply typing, exactly why the arguments passed through untyped. Calling the method directly is equivalent and fully typed. - Same root cause for MacroBuilderModal's this.close.bind(this) and executeMacro.test.ts's .bind() - replaced with equivalent non-bind forms. - confirmDeleteModal/mobileModifyModal: inline the h(...) call into render() instead of round-tripping through an intermediately VNode-typed field, which is where preact's h() lost prop typing. - vitest.config.ts-style import.meta.dirname swapped for fileURLToPath(import.meta.url) in eslint.config.mts, since the project's TS lib doesn't type import.meta.dirname. Type-level only; no behavior change beyond what's already covered by the two preceding commits.
2026-07-11 13:43:37 +00:00
(Object.prototype.hasOwnProperty.call(cmd, "checkCallback") as boolean) ||
(Object.prototype.hasOwnProperty.call(cmd, "editorCheckCallback") as boolean);
2022-06-06 16:29:14 +00:00
const modeIcon = getModeIcon(pair.mode);
2022-07-11 22:07:28 +00:00
const modeName = pair.mode.match(/desktop|mobile|any/)
? pair.mode[0].toUpperCase() + pair.mode.substring(1)
: t("This device");
2022-06-06 16:29:14 +00:00
return (
<Fragment>
2022-07-11 22:07:28 +00:00
{Platform.isDesktop && (
<div className="setting-item mod-toggle">
<ObsidianIcon
icon={pair.icon}
size={20}
aria-label={t("Choose new")}
onClick={handleNewIcon}
className="cmdr-icon clickable-icon"
/>
<div className="setting-item-info">
<div className="setting-item-name">
<ChangeableText
ariaLabel={t("Double click to rename")}
2022-07-11 22:07:28 +00:00
handleChange={({ target }): void => {
lint: replace remaining any-typed casts with proper types Resolves the rest of the no-unsafe-* family by giving TypeScript enough information instead of suppressing it: - Extend the Obsidian module augmentation (types.ts) with App.commands.removeCommand and Vault.getConfig so util.tsx and main.ts no longer need @ts-ignore around them. - Cast event.target to the concrete HTMLElement subtype at each DOM event handler that reads .value/.checked/.offsetWidth (ChangeableText, MacroBuilder, commandComponent) instead of @ts-ignore/@ts-expect-error. - Cast JSON.parse() results to their known shape (MacroBuilder's macro clone, locales.test.ts's loadJson, main.ts's loadSettings) instead of letting `any` flow through. - Drop menuManager's unusual `this: (_item: MenuItem) => void` parameter and the .call(this, ...) it required - the returned closure never reads `this`, so this was both unnecessary and, under this project's non-strict bind/call/apply typing, exactly why the arguments passed through untyped. Calling the method directly is equivalent and fully typed. - Same root cause for MacroBuilderModal's this.close.bind(this) and executeMacro.test.ts's .bind() - replaced with equivalent non-bind forms. - confirmDeleteModal/mobileModifyModal: inline the h(...) call into render() instead of round-tripping through an intermediately VNode-typed field, which is where preact's h() lost prop typing. - vitest.config.ts-style import.meta.dirname swapped for fileURLToPath(import.meta.url) in eslint.config.mts, since the project's TS lib doesn't type import.meta.dirname. Type-level only; no behavior change beyond what's already covered by the two preceding commits.
2026-07-11 13:43:37 +00:00
handleRename(
(target as HTMLInputElement | null)?.value ?? ""
);
}}
2022-07-11 22:07:28 +00:00
value={pair.name}
/>
{pair.name !== cmd.name && (
<span style="margin-left: .8ex">
({cmd.name})
</span>
)}
</div>
<div className="setting-item-description">
{t(
"Added by {{plugin_name}}.".replace(
"{{plugin_name}}",
isInternal ? "Obsidian" : owningPlugin.name
)
)}{" "}
{isChecked
? t(
"Warning: This is a checked Command, meaning it might not run under every circumstance."
)
2022-07-11 22:07:28 +00:00
: ""}
</div>
</div>
<div className="setting-item-control">
<ColorPicker
initialColor={pair.color ?? "#000"}
onChange={handleColorChange}
/>
{sortable && (
<>
<ObsidianIcon
icon="arrow-down"
className="setting-editor-extra-setting-button clickable-icon"
onClick={handleDown}
aria-label={t("Move down")}
/>
<ObsidianIcon
icon="arrow-up"
className="setting-editor-extra-setting-button clickable-icon"
onClick={handleUp}
aria-label={t("Move up")}
/>
</>
)}
2022-07-11 22:07:28 +00:00
<ObsidianIcon
icon={modeIcon}
className="setting-editor-extra-setting-button clickable-icon"
onClick={(): void => handleModeChange()}
2022-07-11 22:07:28 +00:00
aria-label={t(
"Change Mode (Currently: {{current_mode}})"
).replace("{{current_mode}}", modeName)}
/>
<button
className="mod-warning"
style="display: flex"
onClick={handleRemove}
aria-label={t("Delete")}
>
<ObsidianIcon icon="lucide-trash" />
</button>
</div>
2022-06-06 16:29:14 +00:00
</div>
2022-07-11 22:07:28 +00:00
)}
{Platform.isMobile && (
<div className="mobile-option-setting-item">
<span
className="mobile-option-setting-item-remove-icon"
onClick={handleRemove}
>
<ObsidianIcon
icon="minus-with-circle"
size={22}
style={{ color: "var(--text-error)" }}
/>
</span>
<span className="mobile-option-setting-item-option-icon">
2022-07-13 19:47:57 +00:00
<ObsidianIcon
icon={pair.icon}
size={22}
onClick={(): void => {
new MobileModifyModal(
2024-10-11 01:16:41 +00:00
plugin,
pair,
handleRename,
handleNewIcon,
handleModeChange,
handleColorChange
).open();
2022-07-13 19:47:57 +00:00
}}
/>
2022-07-11 22:07:28 +00:00
</span>
<span
className="mobile-option-setting-item-name"
onClick={(): void => {
new MobileModifyModal(
2024-10-11 01:16:41 +00:00
plugin,
pair,
handleRename,
handleNewIcon,
handleModeChange,
handleColorChange
).open();
}}
>
2022-07-11 22:07:28 +00:00
{pair.name}
{pair.name !== cmd.name && (
<span className="cmdr-option-setting-name">
({cmd.name})
</span>
2022-07-11 22:07:28 +00:00
)}
</span>
<span className="mobile-option-setting-item-option-icon">
{sortable && (
<>
<ObsidianIcon
icon="arrow-down"
className="clickable-icon"
onClick={handleDown}
/>
<ObsidianIcon
icon="arrow-up"
className="clickable-icon"
onClick={handleUp}
/>
</>
)}
2022-07-11 22:07:28 +00:00
<ObsidianIcon
icon="three-horizontal-bars"
className="clickable-icon"
onClick={(): void => {
new MobileModifyModal(
2024-10-11 01:16:41 +00:00
plugin,
pair,
handleRename,
handleNewIcon,
handleModeChange,
handleColorChange
).open();
}}
2022-07-11 22:07:28 +00:00
/>
</span>
2022-06-06 16:29:14 +00:00
</div>
2022-07-11 22:07:28 +00:00
)}
2022-06-06 16:29:14 +00:00
</Fragment>
);
}
function getModeIcon(mode: string): string {
if (mode === "mobile") return "smartphone";
if (mode === "desktop") return "monitor";
2022-07-12 15:20:14 +00:00
if (mode === "any") return "cmdr-all-devices";
return "airplay";
}