Delete, rename and change icon by right-clicking and in the settings

This commit is contained in:
phibr0 2022-06-13 11:31:03 +02:00
parent 03730390cd
commit 995baf96ef
13 changed files with 701 additions and 346 deletions

View file

@ -1,7 +1,7 @@
{
"id": "cmdr",
"name": "Commander",
"version": "0.0.3",
"version": "0.0.4",
"minAppVersion": "0.12.0",
"description": "Customize your workspace by adding commands /everywhere/.",
"author": "jsmorabito & phibr0",

626
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "cmdr",
"version": "0.0.3",
"version": "0.0.4",
"description": "Customize your workspace by adding commands /everywhere/.",
"main": "main.js",
"scripts": {

View file

@ -1,9 +1,11 @@
import { Command, Editor, MarkdownView, Menu, MenuItem, TAbstractFile, WorkspaceLeaf } from "obsidian";
import { Command, Editor, MarkdownView, Menu, MenuItem, setIcon, TAbstractFile, WorkspaceLeaf } from "obsidian";
import CommandManager from "./_commandManager";
import CommanderPlugin from "../main";
import { CommandIconPair } from "../types";
import ConfirmDeleteModal from "../ui/confirmDeleteModal";
import { chooseNewCommand, getCommandFromId } from "../util";
import ChooseCustomNameModal from "src/ui/chooseCustomNameModal";
import ChooseIconModal from "src/ui/chooseIconModal";
abstract class Base extends CommandManager {
public async addCommand(pair: CommandIconPair): Promise<void> {
@ -25,14 +27,64 @@ abstract class Base extends CommandManager {
return (item: MenuItem) => {
item.dom.addClass("cmdr");
item.dom.style.display = "flex";
const optionEl = createDiv({
cls: "clickable-icon", attr: { "style": `position: absolute; right: 10px; padding-top: 2px;` }
});
optionEl.addEventListener("click", (event) => {
event.preventDefault();
event.stopImmediatePropagation();
new Menu(app)
.addItem(item => {
item
.setTitle("Change Icon")
.setIcon("box")
.onClick(async () => {
const newIcon = await (new ChooseIconModal(plugin)).awaitSelection();
if (newIcon && newIcon !== cmdPair.icon) {
cmdPair.icon = newIcon;
await plugin.saveSettings();
}
});
})
.addItem(item => {
item
.setTitle("Rename")
.setIcon("text-cursor-input")
.onClick(async () => {
const newName = await (new ChooseCustomNameModal(cmdPair.name)).awaitSelection();
if (newName && newName !== cmdPair.name) {
cmdPair.name = newName;
await plugin.saveSettings();
}
});
})
.addItem(item => {
item.dom.addClass("is-warning");
item
.setTitle("Delete")
.setIcon("lucide-trash")
.onClick(async () => {
commandList.remove(cmdPair);
await plugin.saveSettings();
});
})
.showAtMouseEvent(event);
});
setIcon(optionEl, "more-vertical", 16);
item.dom.append(optionEl);
let isRemovable = false;
const setNormal = (): void => {
optionEl.style.display = "none";
item
.setTitle(cmdPair.name ?? command.name)
.setIcon(cmdPair.icon)
.onClick(() => app.commands.executeCommandById(cmdPair.id));
};
const setRemovable = (): void => {
optionEl.style.display = "block";
item.setTitle("Delete").setIcon("trash").onClick(async (event) => {
event.preventDefault();
event.stopImmediatePropagation();

View file

@ -1,9 +1,14 @@
import { setIcon } from "obsidian";
import { Menu, setIcon } from "obsidian";
import CommanderPlugin from "src/main";
import { CommandIconPair } from "src/types";
import ChooseCustomNameModal from "src/ui/chooseCustomNameModal";
import ChooseIconModal from "src/ui/chooseIconModal";
import { chooseNewCommand } from "src/util";
import CommandManager from "./_commandManager";
export default class PageHeaderManager extends CommandManager {
private addBtn = createDiv({ cls: "cmdr view-action cmdr-adder", attr: { "aria-label": "Add new" } });
public constructor(plugin: CommanderPlugin, pairArray: CommandIconPair[]) {
super(plugin, pairArray);
this.init();
@ -30,13 +35,12 @@ export default class PageHeaderManager extends CommandManager {
private addPageHeaderButton(
viewActions: Element,
button: CommandIconPair
pair: CommandIconPair
): void {
const { id, icon, name } = button;
const ICON_SIZE = 16;
const { id, icon, name } = pair;
const classes = ['view-action', "cmdr-page-header"];
const buttonIcon = this.getButtonIcon(name, id, icon, ICON_SIZE, classes);
const buttonIcon = this.getButtonIcon(name, id, icon, 16, classes);
viewActions.prepend(buttonIcon);
this.plugin.registerDomEvent(buttonIcon, 'click', () => {
@ -47,6 +51,54 @@ export default class PageHeaderManager extends CommandManager {
*/
setTimeout(() => app.commands.executeCommandById(id), 5);
});
buttonIcon.addEventListener("contextmenu", (event) => {
event.stopImmediatePropagation();
new Menu(app)
.addItem(item => {
item
.setTitle("Add Command")
.setIcon("command")
.onClick(async () => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
});
})
.addSeparator()
.addItem(item => {
item
.setTitle("Change Icon")
.setIcon("box")
.onClick(async () => {
const newIcon = await (new ChooseIconModal(this.plugin)).awaitSelection();
if (newIcon && newIcon !== pair.icon) {
pair.icon = newIcon;
await this.plugin.saveSettings();
this.reorder();
}
});
})
.addItem(item => {
item
.setTitle("Rename")
.setIcon("text-cursor-input")
.onClick(async () => {
const newName = await (new ChooseCustomNameModal(pair.name)).awaitSelection();
if (newName && newName !== pair.name) {
pair.name = newName;
await this.plugin.saveSettings();
this.reorder();
}
});
})
.addItem(item => {
item.dom.addClass("is-warning");
item
.setTitle("Delete")
.setIcon("lucide-trash")
.onClick(() => this.removeCommand(pair));
})
.showAtMouseEvent(event);
});
}
private init(): void {
@ -69,6 +121,15 @@ export default class PageHeaderManager extends CommandManager {
);
}
}
this.plugin.register(() => this.addBtn.remove());
setIcon(this.addBtn, "plus");
this.addBtn.onclick = async (): Promise<void> => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
this.reorder();
};
if (this.plugin.settings.showAddCommand) viewActions.prepend(this.addBtn);
}));
}

View file

@ -1,15 +1,18 @@
import { setIcon, WorkspaceRibbon } from "obsidian";
import { Menu, setIcon, WorkspaceRibbon } from "obsidian";
import CommandManager from "./_commandManager";
import CommanderPlugin from "../main";
import { CommandIconPair } from "../types";
import ConfirmDeleteModal from "../ui/confirmDeleteModal";
import { getCommandFromId } from "../util";
import { chooseNewCommand, getCommandFromId } from "../util";
import ChooseCustomNameModal from "src/ui/chooseCustomNameModal";
import ChooseIconModal from "src/ui/chooseIconModal";
export default class RibbonManager extends CommandManager {
private actions: {
[id: string]: HTMLElement;
};
private ribbonEl: WorkspaceRibbon;
private addBtn: HTMLDivElement;
public constructor(
private side: "left" | "right",
@ -23,6 +26,7 @@ export default class RibbonManager extends CommandManager {
if (this.side === "right") {
this.addActionContainer();
}
this.addBtn = createDiv({ cls: "cmdr side-dock-ribbon-action cmdr-adder", attr: { "aria-label-position": side === "left" ? "right" : "left", "aria-label": "Add new" } });
this.init();
});
@ -50,12 +54,22 @@ export default class RibbonManager extends CommandManager {
}
public reorder(): void | Promise<void> {
this.addBtn.remove();
Object.values(this.actions).forEach(el => el.remove());
this.init();
}
private init(): void {
for (const c of this.pairs) this.addAction(c.name, c.icon, c, () => app.commands.executeCommandById(c.id));
this.plugin.register(() => this.addBtn.remove());
setIcon(this.addBtn, "plus");
this.addBtn.onclick = async (): Promise<void> => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
this.reorder();
};
if (this.plugin.settings.showAddCommand) this.ribbonEl.ribbonActionsEl?.append(this.addBtn);
}
// eslint-disable-next-line no-unused-vars
@ -95,6 +109,54 @@ export default class RibbonManager extends CommandManager {
isRemovable = true;
}
});
newAction.addEventListener("contextmenu", (event) => {
event.stopImmediatePropagation();
new Menu(app)
.addItem(item => {
item
.setTitle("Add Command")
.setIcon("command")
.onClick(async () => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
});
})
.addSeparator()
.addItem(item => {
item
.setTitle("Change Icon")
.setIcon("box")
.onClick(async () => {
const newIcon = await (new ChooseIconModal(this.plugin)).awaitSelection();
if (newIcon && newIcon !== pair.icon) {
pair.icon = newIcon;
await this.plugin.saveSettings();
this.reorder();
}
});
})
.addItem(item => {
item
.setTitle("Rename")
.setIcon("text-cursor-input")
.onClick(async () => {
const newName = await (new ChooseCustomNameModal(pair.name)).awaitSelection();
if (newName && newName !== pair.name) {
pair.name = newName;
await this.plugin.saveSettings();
this.reorder();
}
});
})
.addItem(item => {
item.dom.addClass("is-warning");
item
.setTitle("Delete")
.setIcon("lucide-trash")
.onClick(() => this.removeCommand(pair));
})
.showAtMouseEvent(event);
});
setNormal();

View file

@ -1,13 +1,16 @@
import { setIcon } from "obsidian";
import { Menu, setIcon } from "obsidian";
import CommanderPlugin from "src/main";
import { CommandIconPair } from "src/types";
import ChooseCustomNameModal from "src/ui/chooseCustomNameModal";
import ChooseIconModal from "src/ui/chooseIconModal";
import ConfirmDeleteModal from "src/ui/confirmDeleteModal";
import { getCommandFromId } from "src/util";
import { getCommandFromId, chooseNewCommand } from "src/util";
import CommandManager from "./_commandManager";
export default class StatusBarManager extends CommandManager {
private container: HTMLElement;
private readonly actions = new Map<CommandIconPair, HTMLElement>();
private addBtn = createDiv({ cls: "cmdr status-bar-item cmdr-adder", attr: { "aria-label-position": "top", "aria-label": "Add new" } });
public constructor(plugin: CommanderPlugin, pairs: CommandIconPair[]) {
super(plugin, pairs);
@ -26,10 +29,38 @@ export default class StatusBarManager extends CommandManager {
this.addAction(cmd);
}
this.plugin.saveSettings();
this.plugin.registerDomEvent(this.container, "contextmenu", (event) => {
if (event.target !== this.container) {
return;
}
new Menu(app)
.addItem(item => {
item
.setTitle("Add Command")
.setIcon("command")
.onClick(async () => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
});
})
.showAtMouseEvent(event);
});
this.plugin.register(() => this.addBtn.remove());
setIcon(this.addBtn, "plus", 12);
this.addBtn.onclick = async (): Promise<void> => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
this.reorder();
};
if (this.plugin.settings.showAddCommand) this.container.prepend(this.addBtn);
});
}
public reorder(): void {
this.addBtn.remove();
this.actions.forEach((_, key) => this.removeAction(key, true));
this.init();
}
@ -79,6 +110,54 @@ export default class StatusBarManager extends CommandManager {
isRemovable = true;
}
});
btn.addEventListener("contextmenu", (event) => {
event.stopImmediatePropagation();
new Menu(app)
.addItem(item => {
item
.setTitle("Add Command")
.setIcon("command")
.onClick(async () => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
});
})
.addSeparator()
.addItem(item => {
item
.setTitle("Change Icon")
.setIcon("box")
.onClick(async () => {
const newIcon = await (new ChooseIconModal(this.plugin)).awaitSelection();
if (newIcon && newIcon !== pair.icon) {
pair.icon = newIcon;
await this.plugin.saveSettings();
this.reorder();
}
});
})
.addItem(item => {
item
.setTitle("Rename")
.setIcon("text-cursor-input")
.onClick(async () => {
const newName = await (new ChooseCustomNameModal(pair.name)).awaitSelection();
if (newName && newName !== pair.name) {
pair.name = newName;
await this.plugin.saveSettings();
this.reorder();
}
});
})
.addItem(item => {
item.dom.addClass("is-warning");
item
.setTitle("Delete")
.setIcon("lucide-trash")
.onClick(() => this.removeCommand(pair));
})
.showAtMouseEvent(event);
});
setNormal();
this.container.prepend(btn);

View file

@ -1,13 +1,16 @@
import { setIcon } from "obsidian";
import { Menu, setIcon } from "obsidian";
import CommanderPlugin from "src/main";
import { CommandIconPair } from "src/types";
import ChooseCustomNameModal from "src/ui/chooseCustomNameModal";
import ChooseIconModal from "src/ui/chooseIconModal";
import ConfirmDeleteModal from "src/ui/confirmDeleteModal";
import { getCommandFromId } from "src/util";
import { chooseNewCommand, getCommandFromId } from "src/util";
import CommandManager from "./_commandManager";
export default class TitleBarManager extends CommandManager {
private container: HTMLElement;
private readonly actions = new Map<CommandIconPair, HTMLElement>();
private addBtn = createDiv({ cls: "cmdr titlebar-button cmdr-adder", attr: { "aria-label": "Add new" } });
public constructor(plugin: CommanderPlugin, pairArray: CommandIconPair[]) {
super(plugin, pairArray);
@ -27,10 +30,20 @@ export default class TitleBarManager extends CommandManager {
this.addTitleBarAction(cmd);
}
this.plugin.saveSettings();
this.plugin.register(() => this.addBtn.remove());
setIcon(this.addBtn, "plus", 12);
this.addBtn.onclick = async (): Promise<void> => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
this.reorder();
};
if (this.plugin.settings.showAddCommand) this.container.prepend(this.addBtn);
});
}
public reorder(): void {
this.addBtn.remove();
this.actions.forEach((_, key) => this.removeTitleBarAction(key, true));
this.init();
}
@ -74,6 +87,54 @@ export default class TitleBarManager extends CommandManager {
isRemovable = true;
}
});
btn.addEventListener("contextmenu", (event) => {
event.stopImmediatePropagation();
new Menu(app)
.addItem(item => {
item
.setTitle("Add Command")
.setIcon("command")
.onClick(async () => {
const pair = await chooseNewCommand(this.plugin);
this.addCommand(pair);
});
})
.addSeparator()
.addItem(item => {
item
.setTitle("Change Icon")
.setIcon("box")
.onClick(async () => {
const newIcon = await (new ChooseIconModal(this.plugin)).awaitSelection();
if (newIcon && newIcon !== pair.icon) {
pair.icon = newIcon;
await this.plugin.saveSettings();
this.reorder();
}
});
})
.addItem(item => {
item
.setTitle("Rename")
.setIcon("text-cursor-input")
.onClick(async () => {
const newName = await (new ChooseCustomNameModal(pair.name)).awaitSelection();
if (newName && newName !== pair.name) {
pair.name = newName;
await this.plugin.saveSettings();
this.reorder();
}
});
})
.addItem(item => {
item.dom.addClass("is-warning");
item
.setTitle("Delete")
.setIcon("lucide-trash")
.onClick(() => this.removeCommand(pair));
})
.showAtMouseEvent(event);
});
setNormal();
this.container.prepend(btn);

View file

@ -23,10 +23,8 @@
}
.cmdr-icon {
background-color: var(--background-primary);
padding: 4px;
border-radius: 4px;
height: 28px;
height: 20px;
align-self: center;
}
.cmdr-setting-modal > .modal {
@ -62,6 +60,15 @@
justify-content: center;
}
.cmdr-adder {
opacity: 0;
transition: opacity 200ms ease;
&:hover {
opacity: 1;
}
}
.cmdr-macro-builder {
.modal {
height: 80vh;

View file

@ -0,0 +1,34 @@
import { h } from "preact";
import { useState } from "preact/hooks";
interface Props {
value: string;
handleChange: (e: Event) => void;
}
export default function ChangeableText({ value, handleChange }: Props): h.JSX.Element {
const [showInputEle, setShowInput] = useState(false);
return (
<span>
{
showInputEle ? (
<input
type="text"
value={value}
onChange={(e): void => {
setShowInput(false);
handleChange(e);
}}
onBlur={(): void => setShowInput(false)}
autoFocus
/>
) : (
<span onDblClick={(): void => setShowInput(true)} aria-label="Double click to rename">
{value}
</span>
)
}
</span>
);
}

View file

@ -3,15 +3,19 @@ import { Fragment, h } from "preact";
import { useEffect, useRef } from "preact/hooks";
import { CommandIconPair } from "src/types";
import { getCommandFromId } from "src/util";
import ChangeableText from "./ChangeableText";
interface CommandViewerProps {
pair: CommandIconPair;
handleRemove: () => void;
handleUp: () => void;
handleDown: () => void;
handleNewIcon: () => void;
// eslint-disable-next-line no-unused-vars
handleRename: (name: string) => void;
}
export default function CommandComponent({ pair, handleRemove, handleDown, handleUp }: CommandViewerProps): h.JSX.Element {
export default function CommandComponent({ pair, handleRemove, handleDown, handleUp, handleNewIcon, handleRename }: CommandViewerProps): h.JSX.Element {
const cmd = getCommandFromId(pair.id);
if (!cmd) {
// !TODO
@ -28,7 +32,7 @@ export default function CommandComponent({ pair, handleRemove, handleDown, handl
const downIcon = useRef<HTMLDivElement>(null);
const deleteIcon = useRef<HTMLButtonElement>(null);
useEffect(() => {
setIcon(cmdIcon.current!, pair.icon); // eslint-disable-line @typescript-eslint/no-non-null-assertion
setIcon(cmdIcon.current!, pair.icon, 20); // eslint-disable-line @typescript-eslint/no-non-null-assertion
setIcon(upIcon.current!, "arrow-up"); // eslint-disable-line @typescript-eslint/no-non-null-assertion
setIcon(downIcon.current!, "arrow-down"); // eslint-disable-line @typescript-eslint/no-non-null-assertion
setIcon(deleteIcon.current!, "lucide-trash"); // eslint-disable-line @typescript-eslint/no-non-null-assertion
@ -37,9 +41,13 @@ export default function CommandComponent({ pair, handleRemove, handleDown, handl
return (
<Fragment>
<div className="setting-item">
<div ref={cmdIcon} className="cmdr-icon" />
<div ref={cmdIcon} className="cmdr-icon clickable-icon" aria-label="Choose new" onClick={handleNewIcon} />
<div className="setting-item-info">
<div className="setting-item-name">{pair.name === cmd.name ? (isInternal ? cmd.name : cmd.name.split(":").slice(1).join()) : `${pair.name} (${cmd.name})`}</div>
<div className="setting-item-name">
{/* @ts-ignore */}
<ChangeableText handleChange={({ target }): void => handleRename(target?.value)} value={pair.name} />
{pair.name !== cmd.name && <span style="margin-left: .8ex">({cmd.name})</span>}
</div>
<div className="setting-item-description">Added by {isInternal ? "Obsidian" : owningPlugin.name}. {isChecked ? "Warning: This is a checked Command, meaning it might not run under every circumstance." : ""}</div>
</div>
<div className="setting-item-control">

View file

@ -7,6 +7,7 @@ import { chooseNewCommand } from "src/util";
import { arrayMoveMutable } from "array-move";
import { useEffect, useRef } from "preact/hooks";
import { setIcon } from "obsidian";
import ChooseIconModal from "../chooseIconModal";
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
export const ManagerContext = createContext<CommandManager>(null!);
@ -34,6 +35,16 @@ export default function CommandViewer({ manager, plugin }: CommandViewerProps):
handleRemove={async (): Promise<void> => { await manager.removeCommand(cmd); this.forceUpdate(); }}
handleUp={(): void => { arrayMoveMutable(manager.pairs, idx, idx - 1); manager.reorder(); this.forceUpdate(); }}
handleDown={(): void => { arrayMoveMutable(manager.pairs, idx, idx + 1); manager.reorder(); this.forceUpdate(); }}
handleRename={async (name): Promise<void> => { cmd.name = name; await plugin.saveSettings(); manager.reorder(); this.forceUpdate(); }}
handleNewIcon={async (): Promise<void> => {
const newIcon = await (new ChooseIconModal(plugin)).awaitSelection();
if (newIcon && newIcon !== cmd.icon) {
cmd.icon = newIcon;
await plugin.saveSettings();
manager.reorder();
this.forceUpdate();
}
}}
/>;
})}

View file

@ -48,7 +48,7 @@ export default function settingTabComponent({ plugin, mobileMode }: { plugin: Co
<ToggleComponent
value={plugin.settings.showAddCommand}
name='Show "Add Command" Button'
description='Show the "Add Command" Button in every Menu.'
description='Show the "Add Command" Button in every Menu. Requires restart.'
changeHandler={async (value): Promise<void> => {
plugin.settings.showAddCommand = !value;
await plugin.saveSettings();