mirror of
https://github.com/jsmorabito/obsidian-commander.git
synced 2026-07-22 06:40:31 +00:00
feat: Text Formatting Toolbar integration (#197)
feat: Text Formatting Toolbar integration
This commit is contained in:
commit
092d6a534e
10 changed files with 19249 additions and 2 deletions
180
AGENTS.md
Normal file
180
AGENTS.md
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# Obsidian community plugin
|
||||
|
||||
## Project overview
|
||||
|
||||
- Target: Obsidian Community Plugin (TypeScript → bundled JavaScript).
|
||||
- Entry point: `src/main.ts` compiled to `main.js` and loaded by Obsidian.
|
||||
- Required release artifacts: `main.js`, `manifest.json`, and optional `styles.css`.
|
||||
|
||||
## Environment & tooling
|
||||
|
||||
- Node.js: use current LTS (Node 18+ recommended).
|
||||
- **Package manager: npm** (required - `package.json` defines npm scripts and dependencies).
|
||||
- **Bundler: esbuild** (required - `esbuild.config.mjs` and build scripts depend on it).
|
||||
- Types: `obsidian` type definitions.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Dev (watch)
|
||||
|
||||
```bash
|
||||
npm run dev:esbuild
|
||||
```
|
||||
|
||||
### Production build
|
||||
|
||||
```bash
|
||||
npm run build:esbuild
|
||||
```
|
||||
|
||||
## Linting
|
||||
|
||||
- ESLint is preconfigured.
|
||||
- Run `npm run lint` (if available) to lint the project.
|
||||
|
||||
## File & folder conventions
|
||||
|
||||
- Source lives in `src/`. Keep `main.ts` small and focused on plugin lifecycle (loading, unloading, registering commands).
|
||||
- **Current structure**:
|
||||
```
|
||||
src/
|
||||
main.ts # Plugin entry point, lifecycle management
|
||||
types.ts # TypeScript interfaces and types
|
||||
constants.ts # Default settings, static data
|
||||
l10n.ts # Localization (t() function)
|
||||
util.tsx # Utility functions and Preact helpers
|
||||
manager/commands/ # Feature managers (one per UI location)
|
||||
commandManager.ts # Abstract base class
|
||||
leftRibbonManager.ts
|
||||
statusBarManager.ts
|
||||
pageHeaderManager.ts
|
||||
menuManager.ts
|
||||
explorerManager.ts
|
||||
textToolbarManager.ts # Text selection toolbar (new)
|
||||
index.ts
|
||||
ui/
|
||||
settingTab.ts # Registers settings tab with Obsidian
|
||||
settingTabModal.ts
|
||||
icons.ts
|
||||
addCommandModal.ts
|
||||
chooseIconModal.ts
|
||||
chooseCustomNameModal.ts
|
||||
confirmDeleteModal.ts
|
||||
components/ # Preact components
|
||||
settingTabComponent.tsx # Tab container, tab list
|
||||
commandViewerComponent.tsx # Reusable command list with add/remove/reorder
|
||||
commandComponent.tsx # Single command row
|
||||
hidingViewer.tsx # Eye-toggle accordions for hiding native items
|
||||
settingComponent.tsx # ToggleComponent, SliderComponent, EyeToggleComponent
|
||||
TextToolbarSettings.tsx # Text toolbar tab content (new)
|
||||
AdvancedToolbarSettings.tsx
|
||||
MacroViewer.tsx
|
||||
MacroBuilder.tsx
|
||||
Accordion.tsx
|
||||
About.tsx
|
||||
styles/
|
||||
styles.scss
|
||||
advanced-toolbar.scss
|
||||
locale/ # Translation JSON files (en.json is canonical)
|
||||
```
|
||||
- **Do not commit build artifacts**: Never commit `node_modules/`, `main.js`, or other generated files.
|
||||
|
||||
## Key architectural patterns
|
||||
|
||||
### Manager pattern
|
||||
Each UI location (ribbon, status bar, page header, editor menu, etc.) has a manager class that extends `CommandManagerBase`. Managers:
|
||||
- Own the `CommandIconPair[]` array for their location
|
||||
- Implement `addCommand`, `removeCommand`, `reorder`
|
||||
- Handle DOM creation and updates for their location
|
||||
- Are initialized in `main.ts` `onload()` and stored on `plugin.manager`
|
||||
|
||||
### Settings storage
|
||||
Settings are persisted via `plugin.loadData()` / `plugin.saveData()`. The `CommanderSettings` interface in `types.ts` is the source of truth. Always add a nullish guard in `onload()` for any new optional array field so that users upgrading from older versions don't get errors:
|
||||
```typescript
|
||||
this.settings.hide.textToolbar ??= [];
|
||||
```
|
||||
|
||||
### Localization
|
||||
All user-visible strings should go through `t()` from `src/l10n.ts`. The canonical locale is `locale/en.json`. If you add a new UI string, add it to `en.json` (and ideally other locale files). Strings not in the locale fall back to `en.json`; if not found there either, `t()` returns `undefined`, which renders as blank text.
|
||||
|
||||
### Preact UI
|
||||
The settings UI uses Preact (not React). Components live in `src/ui/components/`. Reuse existing components:
|
||||
- `ToggleComponent` / `SliderComponent` / `EyeToggleComponent` from `settingComponent.tsx`
|
||||
- `CommandViewer` from `commandViewerComponent.tsx` — takes a `CommandManagerBase`, renders the command list with add/remove/reorder
|
||||
- `Accordion` from `Accordion.tsx`
|
||||
|
||||
To add a new settings tab:
|
||||
1. Create a Preact component in `src/ui/components/`
|
||||
2. Add an entry to the `tabs` array in `settingTabComponent.tsx`
|
||||
3. The tab name must be a plain string (not `undefined`) — if using `t()`, ensure the key exists in `en.json`
|
||||
|
||||
## Adding a new feature (checklist)
|
||||
|
||||
1. **Types**: Add fields to `CommanderSettings` in `src/types.ts`
|
||||
2. **Defaults**: Add corresponding defaults in `DEFAULT_SETTINGS` in `src/constants.ts`
|
||||
3. **Manager**: Create `src/manager/commands/<feature>Manager.ts` extending `CommandManagerBase`; export from `index.ts`
|
||||
4. **main.ts**: Add nullish guard for any new array fields; instantiate manager; add to `plugin.manager`
|
||||
5. **Settings UI**: Create component in `src/ui/components/`; add tab to `settingTabComponent.tsx`
|
||||
6. **Locale**: Add any new UI strings to `locale/en.json`
|
||||
7. **Styles**: Add CSS classes to `src/styles/styles.scss` using `cmdr-` prefix
|
||||
|
||||
## Text Toolbar feature
|
||||
|
||||
The Text Toolbar (`src/manager/commands/textToolbarManager.ts`) shows a floating action bar above selected text in a markdown source editor. Key implementation notes:
|
||||
|
||||
- Enabled/disabled via `plugin.settings.textToolbarEnabled`
|
||||
- Always instantiated; event handlers check the setting at runtime
|
||||
- Toolbar element is appended to `document.body` with `position: fixed`
|
||||
- Uses `mousedown` + `e.preventDefault()` on buttons to keep editor focus and preserve selection
|
||||
- Checks that selection is inside `.cm-editor` before showing (prevents showing in UI panels)
|
||||
- Desktop-only: mobile guard via `Platform.isMobile` in the manager
|
||||
- Default commands (Bold, Italic, Strikethrough, Copy, Cut) can be individually hidden via `settings.hide.textToolbar`
|
||||
- User-added commands are `CommandIconPair[]` stored in `settings.textToolbar`
|
||||
|
||||
## Manifest rules (`manifest.json`)
|
||||
|
||||
- `id`, `name`, `version` (SemVer), `minAppVersion`, `description`, `isDesktopOnly` are required
|
||||
- Never change `id` after release
|
||||
- Keep `minAppVersion` accurate when using newer APIs
|
||||
|
||||
## Testing
|
||||
|
||||
- Manual install for testing: copy `main.js`, `manifest.json`, `styles.css` (if any) to:
|
||||
```
|
||||
<Vault>/.obsidian/plugins/<plugin-id>/
|
||||
```
|
||||
- Reload Obsidian and enable the plugin in **Settings → Community plugins**
|
||||
- This plugin's source folder IS the installed plugin folder (Dev Vault), so building in place is sufficient — just reload Obsidian after `npm run build:esbuild`
|
||||
|
||||
## Security, privacy, and compliance
|
||||
|
||||
Follow Obsidian's **Developer Policies** and **Plugin Guidelines**:
|
||||
- Default to local/offline operation. Only make network requests when essential.
|
||||
- No hidden telemetry.
|
||||
- Never execute remote code or auto-update plugin code outside of normal releases.
|
||||
- Register and clean up all DOM, app, and interval listeners using `register*` helpers.
|
||||
|
||||
## Performance
|
||||
|
||||
- Keep startup light. Defer heavy work until `onLayoutReady`.
|
||||
- Debounce expensive operations (e.g. selection-change handlers).
|
||||
- Batch disk access; avoid excessive vault scans.
|
||||
|
||||
## Coding conventions
|
||||
|
||||
- TypeScript; existing codebase uses `"strict": false` but prefer strict-compatible code.
|
||||
- CSS class prefix: `cmdr-` for all plugin-owned classes.
|
||||
- Keep `main.ts` minimal — delegate all feature logic to managers and UI components.
|
||||
- `this.register*` helpers for everything that needs cleanup.
|
||||
- Prefer `async/await`; handle errors gracefully.
|
||||
|
||||
## References
|
||||
|
||||
- Obsidian sample plugin: https://github.com/obsidianmd/obsidian-sample-plugin
|
||||
- API documentation: https://docs.obsidian.md
|
||||
- Developer policies: https://docs.obsidian.md/Developer+policies
|
||||
- Plugin guidelines: https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines
|
||||
|
|
@ -88,6 +88,13 @@ const buildOptions = {
|
|||
name: "Move output",
|
||||
setup(build) {
|
||||
build.onEnd(() => {
|
||||
// Always copy main.css → styles.css locally so Obsidian loads current styles
|
||||
try {
|
||||
copyFileSync("main.css", "styles.css");
|
||||
} catch (error) {
|
||||
console.error("Failed to copy main.css → styles.css:", error);
|
||||
}
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const DEFAULT_SETTINGS: CommanderSettings = {
|
|||
pageHeader: [],
|
||||
macros: [],
|
||||
explorer: [],
|
||||
textToolbarCommands: [],
|
||||
hide: {
|
||||
statusbar: [],
|
||||
leftRibbon: [],
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
FileMenuCommandManager,
|
||||
PageHeaderManager,
|
||||
StatusBarManager,
|
||||
TextToolbarIntegrationManager,
|
||||
} from "./manager/commands";
|
||||
import { Action, CommanderSettings } from "./types";
|
||||
import CommanderSettingTab from "./ui/settingTab";
|
||||
|
|
@ -36,6 +37,7 @@ export default class CommanderPlugin extends Plugin {
|
|||
statusBar: StatusBarManager;
|
||||
pageHeader: PageHeaderManager;
|
||||
explorerManager: ExplorerManager;
|
||||
textToolbarIntegration: TextToolbarIntegrationManager;
|
||||
};
|
||||
|
||||
public async executeStartupMacros(): Promise<void> {
|
||||
|
|
@ -95,6 +97,7 @@ export default class CommanderPlugin extends Plugin {
|
|||
statusBar: new StatusBarManager(this, this.settings.statusBar),
|
||||
pageHeader: new PageHeaderManager(this, this.settings.pageHeader),
|
||||
explorerManager: new ExplorerManager(this, this.settings.explorer),
|
||||
textToolbarIntegration: new TextToolbarIntegrationManager(this, this.settings.textToolbarCommands),
|
||||
};
|
||||
|
||||
this.addSettingTab(new CommanderSettingTab(this));
|
||||
|
|
@ -127,6 +130,9 @@ export default class CommanderPlugin extends Plugin {
|
|||
injectIcons(this.settings.advancedToolbar, this);
|
||||
|
||||
this.executeStartupMacros();
|
||||
|
||||
// Push saved commands into the Text Toolbar plugin if it is installed
|
||||
this.manager.textToolbarIntegration.reorder();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import ExplorerManager from "./explorerManager";
|
||||
import TextToolbarIntegrationManager from "./textToolbarIntegrationManager";
|
||||
import {
|
||||
EditorMenuCommandManager,
|
||||
FileMenuCommandManager,
|
||||
|
|
@ -12,6 +13,7 @@ export {
|
|||
FileMenuCommandManager,
|
||||
PageHeaderManager,
|
||||
StatusBarManager,
|
||||
TextToolbarIntegrationManager,
|
||||
TitleBarManager,
|
||||
ExplorerManager,
|
||||
};
|
||||
|
|
|
|||
44
src/manager/commands/textToolbarIntegrationManager.ts
Normal file
44
src/manager/commands/textToolbarIntegrationManager.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import CommanderPlugin from "src/main";
|
||||
import { CommandIconPair } from "src/types";
|
||||
import CommandManagerBase from "./commandManager";
|
||||
|
||||
interface TextToolbarAPI {
|
||||
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;
|
||||
}
|
||||
|
||||
export default class TextToolbarIntegrationManager extends CommandManagerBase {
|
||||
public constructor(plugin: CommanderPlugin, pairs: CommandIconPair[]) {
|
||||
super(plugin, pairs);
|
||||
}
|
||||
|
||||
private sync(): void {
|
||||
const api = getTextToolbarAPI(this.plugin);
|
||||
if (!api) return;
|
||||
api.setCommands(this.pairs.map(p => ({ id: p.id, icon: p.icon, name: p.name })));
|
||||
}
|
||||
|
||||
public static isAvailable(plugin: CommanderPlugin): boolean {
|
||||
return getTextToolbarAPI(plugin) !== undefined;
|
||||
}
|
||||
|
||||
public async addCommand(pair: CommandIconPair): Promise<void> {
|
||||
this.pairs.push(pair);
|
||||
await this.plugin.saveSettings();
|
||||
this.sync();
|
||||
}
|
||||
|
||||
public async removeCommand(pair: CommandIconPair): Promise<void> {
|
||||
this.pairs.remove(pair);
|
||||
await this.plugin.saveSettings();
|
||||
this.sync();
|
||||
}
|
||||
|
||||
public reorder(): void {
|
||||
this.sync();
|
||||
}
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ export interface CommanderSettings {
|
|||
pageHeader: CommandIconPair[];
|
||||
explorer: CommandIconPair[];
|
||||
macros: Macro[];
|
||||
textToolbarCommands: CommandIconPair[];
|
||||
hide: {
|
||||
statusbar: string[];
|
||||
leftRibbon: string[];
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import CommandViewer from "./commandViewerComponent";
|
|||
import { LeftRibbonHider, StatusbarHider } from "./hidingViewer";
|
||||
import MacroViewer from "./MacroViewer";
|
||||
import { SliderComponent, ToggleComponent } from "./settingComponent";
|
||||
import TextToolbarIntegrationManager from "src/manager/commands/textToolbarIntegrationManager";
|
||||
|
||||
export default function settingTabComponent({
|
||||
plugin,
|
||||
|
|
@ -113,6 +114,7 @@ export default function settingTabComponent({
|
|||
await plugin.saveSettings();
|
||||
}}
|
||||
/>
|
||||
|
||||
</Fragment>
|
||||
),
|
||||
},
|
||||
|
|
@ -254,6 +256,20 @@ export default function settingTabComponent({
|
|||
</CommandViewer>
|
||||
),
|
||||
},
|
||||
|
||||
...(TextToolbarIntegrationManager.isAvailable(plugin)
|
||||
? [
|
||||
{
|
||||
name: "Text Toolbar",
|
||||
tab: (
|
||||
<CommandViewer
|
||||
manager={plugin.manager.textToolbarIntegration}
|
||||
plugin={plugin}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: Platform.isMobile ? "Mobile Toolbar" : "Toolbar",
|
||||
tab: <AdvancedToolbarSettings plugin={plugin} />,
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
"0.4.1": "0.16.0",
|
||||
"0.1.1": "0.16.0",
|
||||
"0.1.0": "0.15.0",
|
||||
"0.5.2": "1.4.0"
|
||||
}
|
||||
"0.5.2": "1.4.0",
|
||||
"0.5.6": "1.4.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue