diff --git a/.editorconfig b/.editorconfig index 84b8a66..d27d8fe 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,6 +5,5 @@ root = true charset = utf-8 end_of_line = lf insert_final_newline = true -indent_style = tab -indent_size = 4 -tab_width = 4 +indent_style = space +indent_size = 2 diff --git a/src/settings.ts b/src/settings.ts index 8969b30..443ebd1 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,96 +1,96 @@ import { - App, - PluginSettingTab, - Setting, - FuzzySuggestModal, - TFolder, - Command, - SuggestModal, - ExtraButtonComponent, - getIconIds, + App, + PluginSettingTab, + Setting, + FuzzySuggestModal, + TFolder, + Command, + SuggestModal, + ExtraButtonComponent, + getIconIds, } from "obsidian"; import MobilePlugin from "./main"; export interface ToolbarConfig { - id: string; - name: string; - commands: string[]; + id: string; + name: string; + commands: string[]; } export type ContextType = - | "selection" - | "list" - | "task" - | "heading" - | "code-block" - | "table" - | "blockquote" - | "link" - | "default"; + | "selection" + | "list" + | "task" + | "heading" + | "code-block" + | "table" + | "blockquote" + | "link" + | "default"; export interface ContextBinding { - id: string; - contextType: ContextType; - toolbarId: string; + id: string; + contextType: ContextType; + toolbarId: string; } export interface MobilePluginSettings { - homeFolder: string; - toolbarCommands: string[]; // Deprecated - kept for backward compatibility - toolbars: ToolbarConfig[]; - contextBindings: ContextBinding[]; - useIcons: boolean; - commandIcons: Record; // Map of command ID to icon name - enableHapticFeedback: boolean; + homeFolder: string; + toolbarCommands: string[]; // Deprecated - kept for backward compatibility + toolbars: ToolbarConfig[]; + contextBindings: ContextBinding[]; + useIcons: boolean; + commandIcons: Record; // Map of command ID to icon name + enableHapticFeedback: boolean; } export const DEFAULT_SETTINGS: MobilePluginSettings = { - homeFolder: "", - toolbarCommands: [ - "editor:toggle-bold", - "editor:toggle-italics", - "editor:insert-link", - ], - toolbars: [ - { - id: "formatting", - name: "Formatting", - commands: [ - "editor:toggle-bold", - "editor:toggle-italics", - "editor:insert-link", - ], - }, - { - id: "list-actions", - name: "List Actions", - commands: [ - "editor:toggle-checklist-status", - "editor:indent-list", - "editor:unindent-list", - ], - }, - ], - contextBindings: [ - { - id: "binding-selection", - contextType: "selection", - toolbarId: "formatting", - }, - { - id: "binding-list", - contextType: "list", - toolbarId: "list-actions", - }, - { - id: "binding-default", - contextType: "default", - toolbarId: "formatting", - }, - ], - useIcons: false, - commandIcons: {}, - enableHapticFeedback: true, + homeFolder: "", + toolbarCommands: [ + "editor:toggle-bold", + "editor:toggle-italics", + "editor:insert-link", + ], + toolbars: [ + { + id: "formatting", + name: "Formatting", + commands: [ + "editor:toggle-bold", + "editor:toggle-italics", + "editor:insert-link", + ], + }, + { + id: "list-actions", + name: "List Actions", + commands: [ + "editor:toggle-checklist-status", + "editor:indent-list", + "editor:unindent-list", + ], + }, + ], + contextBindings: [ + { + id: "binding-selection", + contextType: "selection", + toolbarId: "formatting", + }, + { + id: "binding-list", + contextType: "list", + toolbarId: "list-actions", + }, + { + id: "binding-default", + contextType: "default", + toolbarId: "formatting", + }, + ], + useIcons: false, + commandIcons: {}, + enableHapticFeedback: true, }; /** @@ -116,514 +116,492 @@ export const DEFAULT_SETTINGS: MobilePluginSettings = { * @param prompt - Optional placeholder text for the search input. */ export class FolderSuggest extends FuzzySuggestModal { - onSubmit: (result: TFolder) => void; - folders: TFolder[]; - constructor( - app: App, - onSubmit: (result: TFolder) => void, - prompt?: string - ) { - super(app); - this.onSubmit = onSubmit; - this.folders = app.vault - .getAllLoadedFiles() - .filter((f) => f instanceof TFolder) as TFolder[]; - this.setPlaceholder(prompt || "Search for a folder..."); - } - getItems(): TFolder[] { - return this.folders; - } - getItemText(folder: TFolder): string { - return folder.path; - } - onChooseItem(folder: TFolder, evt: MouseEvent | KeyboardEvent) { - this.onSubmit(folder); - } + onSubmit: (result: TFolder) => void; + folders: TFolder[]; + constructor(app: App, onSubmit: (result: TFolder) => void, prompt?: string) { + super(app); + this.onSubmit = onSubmit; + this.folders = app.vault + .getAllLoadedFiles() + .filter((f) => f instanceof TFolder) as TFolder[]; + this.setPlaceholder(prompt || "Search for a folder..."); + } + getItems(): TFolder[] { + return this.folders; + } + getItemText(folder: TFolder): string { + return folder.path; + } + onChooseItem(folder: TFolder, evt: MouseEvent | KeyboardEvent) { + this.onSubmit(folder); + } } export class IconSuggestModal extends SuggestModal { - onSubmit: (result: string) => void; - icons: string[]; - getSuggestions(query: string): string[] | Promise { - const lowerQuery = query.toLowerCase(); - return this.icons.filter((icon) => - icon.toLowerCase().includes(lowerQuery) - ); - } + onSubmit: (result: string) => void; + icons: string[]; + getSuggestions(query: string): string[] | Promise { + const lowerQuery = query.toLowerCase(); + return this.icons.filter((icon) => icon.toLowerCase().includes(lowerQuery)); + } - renderSuggestion(value: string, el: HTMLElement): void { - new Setting(el) - .setName(value) - .addExtraButton((btn) => btn.setIcon(`${value}`)); - } + renderSuggestion(value: string, el: HTMLElement): void { + new Setting(el) + .setName(value) + .addExtraButton((btn) => btn.setIcon(`${value}`)); + } - constructor(app: App, onSubmit: (result: string) => void) { - super(app); - this.onSubmit = onSubmit; - getIconIds(); - // Use a curated list of common Lucide icons available in Obsidian - this.icons = getIconIds(); + constructor(app: App, onSubmit: (result: string) => void) { + super(app); + this.onSubmit = onSubmit; + getIconIds(); + // Use a curated list of common Lucide icons available in Obsidian + this.icons = getIconIds(); - this.setPlaceholder("Search for an icon..."); - } + this.setPlaceholder("Search for an icon..."); + } - getItems(): string[] { - return this.icons; - } + getItems(): string[] { + return this.icons; + } - getItemText(item: string): string { - return item; - } + getItemText(item: string): string { + return item; + } - onChooseItem(item: string, evt: MouseEvent | KeyboardEvent) { - this.onSubmit(item); - } + onChooseItem(item: string, evt: MouseEvent | KeyboardEvent) { + this.onSubmit(item); + } - onChooseSuggestion(item: string, evt: MouseEvent | KeyboardEvent) { - this.onSubmit(item); - } + onChooseSuggestion(item: string, evt: MouseEvent | KeyboardEvent) { + this.onSubmit(item); + } } export class CommandSuggestModal extends FuzzySuggestModal { - onSubmit: (result: Command) => void; - commands: Command[]; + onSubmit: (result: Command) => void; + commands: Command[]; - constructor(app: App, onSubmit: (result: Command) => void) { - super(app); - this.onSubmit = onSubmit; - // @ts-ignore - this.commands = Object.values(this.app.commands.commands); - } + constructor(app: App, onSubmit: (result: Command) => void) { + super(app); + this.onSubmit = onSubmit; + // @ts-ignore + this.commands = Object.values(this.app.commands.commands); + } - getItems(): Command[] { - return this.commands; - } + getItems(): Command[] { + return this.commands; + } - getItemText(item: Command): string { - return item.name; - } + getItemText(item: Command): string { + return item.name; + } - onChooseItem(item: Command, evt: MouseEvent | KeyboardEvent) { - this.onSubmit(item); - } + onChooseItem(item: Command, evt: MouseEvent | KeyboardEvent) { + this.onSubmit(item); + } } export class MobileSettingTab extends PluginSettingTab { - plugin: MobilePlugin; + plugin: MobilePlugin; - constructor(app: App, plugin: MobilePlugin) { - super(app, plugin); - this.plugin = plugin; - } + constructor(app: App, plugin: MobilePlugin) { + super(app, plugin); + this.plugin = plugin; + } - display(): void { - const { containerEl } = this; + display(): void { + const { containerEl } = this; - containerEl.empty(); + containerEl.empty(); - // Home folder setting - new Setting(containerEl) - .setName("Home folder") - .setDesc( - "Folder where new notes will be created. Leave empty for vault root." - ) - .addButton((button) => - button - .setButtonText( - this.plugin.settings.homeFolder || "Select folder" - ) - .onClick(() => { - new FolderSuggest( - this.app, - (folder) => { - this.plugin.settings.homeFolder = folder.path; - this.plugin.saveSettings(); - button.setButtonText(folder.path); - }, - "Select a home folder" - ).open(); - }) - ) - .addExtraButton((button) => - button - .setIcon("cross") - .setTooltip("Clear folder") - .onClick(async () => { - this.plugin.settings.homeFolder = ""; - await this.plugin.saveSettings(); - this.display(); - }) - ); + // Home folder setting + new Setting(containerEl) + .setName("Home folder") + .setDesc( + "Folder where new notes will be created. Leave empty for vault root." + ) + .addButton((button) => + button + .setButtonText(this.plugin.settings.homeFolder || "Select folder") + .onClick(() => { + new FolderSuggest( + this.app, + (folder) => { + this.plugin.settings.homeFolder = folder.path; + this.plugin.saveSettings(); + button.setButtonText(folder.path); + }, + "Select a home folder" + ).open(); + }) + ) + .addExtraButton((button) => + button + .setIcon("cross") + .setTooltip("Clear folder") + .onClick(async () => { + this.plugin.settings.homeFolder = ""; + await this.plugin.saveSettings(); + this.display(); + }) + ); - // Use icons setting - new Setting(containerEl) - .setName("Use icons in toolbar") - .setDesc( - "Display icons instead of text labels for toolbar commands. You can customize icons for each command below." - ) - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.useIcons) - .onChange(async (value) => { - this.plugin.settings.useIcons = value; - await this.plugin.saveSettings(); - // Trigger toolbar refresh - this.plugin.refreshToolbar(); - }) - ); + // Use icons setting + new Setting(containerEl) + .setName("Use icons in toolbar") + .setDesc( + "Display icons instead of text labels for toolbar commands. You can customize icons for each command below." + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.useIcons) + .onChange(async (value) => { + this.plugin.settings.useIcons = value; + await this.plugin.saveSettings(); + // Trigger toolbar refresh + this.plugin.refreshToolbar(); + }) + ); - // Haptic feedback setting - new Setting(containerEl) - .setName("Enable haptic feedback") - .setDesc( - "Vibrate on FAB and toolbar button interactions (mobile devices only)" - ) - .addToggle((toggle) => - toggle - .setValue(this.plugin.settings.enableHapticFeedback) - .onChange(async (value) => { - this.plugin.settings.enableHapticFeedback = value; - await this.plugin.saveSettings(); - }) - ); + // Haptic feedback setting + new Setting(containerEl) + .setName("Enable haptic feedback") + .setDesc( + "Vibrate on FAB and toolbar button interactions (mobile devices only)" + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.enableHapticFeedback) + .onChange(async (value) => { + this.plugin.settings.enableHapticFeedback = value; + await this.plugin.saveSettings(); + }) + ); - // Section 1: Define Toolbars - new Setting(containerEl) - .setHeading() - .setName("Toolbar Library") - .setDesc( - "Define toolbars with custom command sets. These can be bound to different contexts below." - ); + // Section 1: Define Toolbars + new Setting(containerEl) + .setHeading() + .setName("Toolbar Library") + .setDesc( + "Define toolbars with custom command sets. These can be bound to different contexts below." + ); - // Render all toolbars - this.plugin.settings.toolbars.forEach((toolbar, toolbarIndex) => { - this.renderToolbar(containerEl, toolbar, toolbarIndex); - }); + // Render all toolbars + this.plugin.settings.toolbars.forEach((toolbar, toolbarIndex) => { + this.renderToolbar(containerEl, toolbar, toolbarIndex); + }); - // Add new toolbar button - new Setting(containerEl).addButton((button) => - button - .setButtonText("Add new toolbar") - .setCta() - .onClick(async () => { - const newToolbar: ToolbarConfig = { - id: `toolbar-${Date.now()}`, - name: `New Toolbar`, - commands: [], - }; - this.plugin.settings.toolbars.push(newToolbar); - await this.plugin.saveSettings(); - this.display(); - }) - ); + // Add new toolbar button + new Setting(containerEl).addButton((button) => + button + .setButtonText("Add new toolbar") + .setCta() + .onClick(async () => { + const newToolbar: ToolbarConfig = { + id: `toolbar-${Date.now()}`, + name: `New Toolbar`, + commands: [], + }; + this.plugin.settings.toolbars.push(newToolbar); + await this.plugin.saveSettings(); + this.display(); + }) + ); - // Section 2: Context Bindings - new Setting(containerEl) - .setHeading() - .setName("Context Bindings") - .setDesc( - "Bind toolbars to different editing contexts. Multiple toolbars bound to the same context will be automatically concatenated." - ); + // Section 2: Context Bindings + new Setting(containerEl) + .setHeading() + .setName("Context Bindings") + .setDesc( + "Bind toolbars to different editing contexts. Multiple toolbars bound to the same context will be automatically concatenated." + ); - // Render all context bindings - this.plugin.settings.contextBindings.forEach( - (binding, bindingIndex) => { - this.renderContextBinding(containerEl, binding, bindingIndex); - } - ); + // Render all context bindings + this.plugin.settings.contextBindings.forEach((binding, bindingIndex) => { + this.renderContextBinding(containerEl, binding, bindingIndex); + }); - // Add new binding button - new Setting(containerEl).addButton((button) => - button - .setButtonText("Add context binding") - .setCta() - .onClick(async () => { - const newBinding: ContextBinding = { - id: `binding-${Date.now()}`, - contextType: "default", - toolbarId: this.plugin.settings.toolbars[0]?.id || "", - }; - this.plugin.settings.contextBindings.push(newBinding); - await this.plugin.saveSettings(); - this.display(); - }) - ); - } + // Add new binding button + new Setting(containerEl).addButton((button) => + button + .setButtonText("Add context binding") + .setCta() + .onClick(async () => { + const newBinding: ContextBinding = { + id: `binding-${Date.now()}`, + contextType: "default", + toolbarId: this.plugin.settings.toolbars[0]?.id || "", + }; + this.plugin.settings.contextBindings.push(newBinding); + await this.plugin.saveSettings(); + this.display(); + }) + ); + } - renderToolbar( - container: HTMLElement, - toolbar: ToolbarConfig, - toolbarIndex: number - ) { - const toolbarSection = container.createDiv("mobile-toolbar-section"); + renderToolbar( + container: HTMLElement, + toolbar: ToolbarConfig, + toolbarIndex: number + ) { + const toolbarSection = container.createDiv("mobile-toolbar-section"); - // Toolbar header with name - const headerSetting = new Setting(toolbarSection) - .setName(toolbar.name) - .setDesc(`ID: ${toolbar.id}`) - .addText((text) => - text - .setPlaceholder("Toolbar name") - .setValue(toolbar.name) - .onChange(async (value) => { - toolbar.name = value; - await this.plugin.saveSettings(); - }) - ) - .addExtraButton((btn) => - btn - .setIcon("trash") - .setTooltip("Delete toolbar") - .onClick(async () => { - // Remove toolbar and any bindings using it - this.plugin.settings.toolbars.splice(toolbarIndex, 1); - this.plugin.settings.contextBindings = - this.plugin.settings.contextBindings.filter( - (b) => b.toolbarId !== toolbar.id - ); - await this.plugin.saveSettings(); - this.display(); - }) - ); + // Toolbar header with name + const headerSetting = new Setting(toolbarSection) + .setName(toolbar.name) + .setDesc(`ID: ${toolbar.id}`) + .addText((text) => + text + .setPlaceholder("Toolbar name") + .setValue(toolbar.name) + .onChange(async (value) => { + toolbar.name = value; + await this.plugin.saveSettings(); + }) + ) + .addExtraButton((btn) => + btn + .setIcon("trash") + .setTooltip("Delete toolbar") + .onClick(async () => { + // Remove toolbar and any bindings using it + this.plugin.settings.toolbars.splice(toolbarIndex, 1); + this.plugin.settings.contextBindings = + this.plugin.settings.contextBindings.filter( + (b) => b.toolbarId !== toolbar.id + ); + await this.plugin.saveSettings(); + this.display(); + }) + ); - headerSetting.settingEl.addClass("mobile-toolbar-header"); + headerSetting.settingEl.addClass("mobile-toolbar-header"); - // Command list for this toolbar - const commandListContainer = toolbarSection.createDiv( - "mobile-command-list" - ); - this.renderCommandListForToolbar( - commandListContainer, - toolbar, - toolbarIndex - ); + // Command list for this toolbar + const commandListContainer = toolbarSection.createDiv( + "mobile-command-list" + ); + this.renderCommandListForToolbar( + commandListContainer, + toolbar, + toolbarIndex + ); - // Add command button for this toolbar - new Setting(toolbarSection).addButton((button) => - button - .setButtonText("Add command") - .setClass("mobile-add-command-btn") - .onClick(() => { - new CommandSuggestModal(this.app, async (command) => { - toolbar.commands.push(command.id); - await this.plugin.saveSettings(); - this.renderCommandListForToolbar( - commandListContainer, - toolbar, - toolbarIndex - ); - }).open(); - }) - ); - } + // Add command button for this toolbar + new Setting(toolbarSection).addButton((button) => + button + .setButtonText("Add command") + .setClass("mobile-add-command-btn") + .onClick(() => { + new CommandSuggestModal(this.app, async (command) => { + toolbar.commands.push(command.id); + await this.plugin.saveSettings(); + this.renderCommandListForToolbar( + commandListContainer, + toolbar, + toolbarIndex + ); + }).open(); + }) + ); + } - renderContextBinding( - container: HTMLElement, - binding: ContextBinding, - bindingIndex: number - ) { - const toolbar = this.plugin.settings.toolbars.find( - (t) => t.id === binding.toolbarId - ); - const toolbarName = toolbar ? toolbar.name : "(Not found)"; + renderContextBinding( + container: HTMLElement, + binding: ContextBinding, + bindingIndex: number + ) { + const toolbar = this.plugin.settings.toolbars.find( + (t) => t.id === binding.toolbarId + ); + const toolbarName = toolbar ? toolbar.name : "(Not found)"; - const setting = new Setting(container) - .setName( - `${this.getContextDisplayName( - binding.contextType - )} → ${toolbarName}` - ) - .addDropdown((dropdown) => { - dropdown - .addOption("selection", "Selection") - .addOption("list", "List") - .addOption("task", "Task") - .addOption("heading", "Heading") - .addOption("code-block", "Code Block") - .addOption("table", "Table") - .addOption("blockquote", "Blockquote") - .addOption("link", "Link") - .addOption("default", "Default") - .setValue(binding.contextType) - .onChange(async (value) => { - binding.contextType = value as ContextType; - await this.plugin.saveSettings(); - this.display(); - }); - }) - .addDropdown((dropdown) => { - this.plugin.settings.toolbars.forEach((toolbar) => { - dropdown.addOption(toolbar.id, toolbar.name); - }); - dropdown.setValue(binding.toolbarId).onChange(async (value) => { - binding.toolbarId = value; - await this.plugin.saveSettings(); - this.display(); - }); - }) - .addExtraButton((btn) => - btn - .setIcon("trash") - .setTooltip("Delete binding") - .onClick(async () => { - this.plugin.settings.contextBindings.splice( - bindingIndex, - 1 - ); - await this.plugin.saveSettings(); - this.display(); - }) - ); - } + const setting = new Setting(container) + .setName( + `${this.getContextDisplayName(binding.contextType)} → ${toolbarName}` + ) + .addDropdown((dropdown) => { + dropdown + .addOption("selection", "Selection") + .addOption("list", "List") + .addOption("task", "Task") + .addOption("heading", "Heading") + .addOption("code-block", "Code Block") + .addOption("table", "Table") + .addOption("blockquote", "Blockquote") + .addOption("link", "Link") + .addOption("default", "Default") + .setValue(binding.contextType) + .onChange(async (value) => { + binding.contextType = value as ContextType; + await this.plugin.saveSettings(); + this.display(); + }); + }) + .addDropdown((dropdown) => { + this.plugin.settings.toolbars.forEach((toolbar) => { + dropdown.addOption(toolbar.id, toolbar.name); + }); + dropdown.setValue(binding.toolbarId).onChange(async (value) => { + binding.toolbarId = value; + await this.plugin.saveSettings(); + this.display(); + }); + }) + .addExtraButton((btn) => + btn + .setIcon("trash") + .setTooltip("Delete binding") + .onClick(async () => { + this.plugin.settings.contextBindings.splice(bindingIndex, 1); + await this.plugin.saveSettings(); + this.display(); + }) + ); + } - getContextDisplayName(contextType: ContextType): string { - const names: Record = { - selection: "Selection", - list: "List", - task: "Task", - heading: "Heading", - "code-block": "Code Block", - table: "Table", - blockquote: "Blockquote", - link: "Link", - default: "Default", - }; - return names[contextType] || contextType; - } + getContextDisplayName(contextType: ContextType): string { + const names: Record = { + selection: "Selection", + list: "List", + task: "Task", + heading: "Heading", + "code-block": "Code Block", + table: "Table", + blockquote: "Blockquote", + link: "Link", + default: "Default", + }; + return names[contextType] || contextType; + } - renderCommandListForToolbar( - container: HTMLElement, - toolbar: ToolbarConfig, - toolbarIndex: number - ) { - container.empty(); - const commands = toolbar.commands; + renderCommandListForToolbar( + container: HTMLElement, + toolbar: ToolbarConfig, + toolbarIndex: number + ) { + container.empty(); + const commands = toolbar.commands; - commands.forEach((cmdId, index) => { - // @ts-ignore - const command = this.app.commands.findCommand(cmdId); - const commandName = command ? command.name : cmdId; - const defaultIcon = command?.icon || ""; - const customIcon = - this.plugin.settings.commandIcons[cmdId] || defaultIcon; + commands.forEach((cmdId, index) => { + // @ts-ignore + const command = this.app.commands.findCommand(cmdId); + const commandName = command ? command.name : cmdId; + const defaultIcon = command?.icon || ""; + const customIcon = + this.plugin.settings.commandIcons[cmdId] || defaultIcon; - const setting = new Setting(container) - .setName(commandName) - .setDesc(cmdId) - .addButton((btn) => - btn - .setIcon(customIcon || "question") - .setTooltip("Change icon") - .onClick(() => { - new IconSuggestModal(this.app, async (icon) => { - this.plugin.settings.commandIcons[cmdId] = icon; - await this.plugin.saveSettings(); - this.renderCommandListForToolbar( - container, - toolbar, - toolbarIndex - ); - }).open(); - }) - ) - .addExtraButton((btn) => - btn - .setIcon("pencil") - .setTooltip("Change command") - .onClick(async () => { - new CommandSuggestModal( - this.app, - async (command) => { - toolbar.commands[index] = command.id; - await this.plugin.saveSettings(); - this.renderCommandListForToolbar( - container, - toolbar, - toolbarIndex - ); - } - ).open(); - }) - ) - .addExtraButton((btn) => - btn - .setIcon("trash") - .setTooltip("Remove command") - .onClick(async () => { - toolbar.commands.splice(index, 1); - await this.plugin.saveSettings(); - this.renderCommandListForToolbar( - container, - toolbar, - toolbarIndex - ); - }) - ); + const setting = new Setting(container) + .setName(commandName) + .setDesc(cmdId) + .addButton((btn) => + btn + .setIcon(customIcon || "question") + .setTooltip("Change icon") + .onClick(() => { + new IconSuggestModal(this.app, async (icon) => { + this.plugin.settings.commandIcons[cmdId] = icon; + await this.plugin.saveSettings(); + this.renderCommandListForToolbar( + container, + toolbar, + toolbarIndex + ); + }).open(); + }) + ) + .addExtraButton((btn) => + btn + .setIcon("pencil") + .setTooltip("Change command") + .onClick(async () => { + new CommandSuggestModal(this.app, async (command) => { + toolbar.commands[index] = command.id; + await this.plugin.saveSettings(); + this.renderCommandListForToolbar( + container, + toolbar, + toolbarIndex + ); + }).open(); + }) + ) + .addExtraButton((btn) => + btn + .setIcon("trash") + .setTooltip("Remove command") + .onClick(async () => { + toolbar.commands.splice(index, 1); + await this.plugin.saveSettings(); + this.renderCommandListForToolbar( + container, + toolbar, + toolbarIndex + ); + }) + ); - const el = setting.settingEl; - el.draggable = true; - el.addClass("mobile-plugin-draggable-item"); + const el = setting.settingEl; + el.draggable = true; + el.addClass("mobile-plugin-draggable-item"); - el.ondragstart = (event) => { - event.dataTransfer?.setData("text/plain", index.toString()); - el.addClass("is-dragging"); - }; + el.ondragstart = (event) => { + event.dataTransfer?.setData("text/plain", index.toString()); + el.addClass("is-dragging"); + }; - el.ondragend = () => { - el.removeClass("is-dragging"); - }; + el.ondragend = () => { + el.removeClass("is-dragging"); + }; - el.ondragover = (event) => { - event.preventDefault(); - const rect = el.getBoundingClientRect(); - const midY = rect.top + rect.height / 2; + el.ondragover = (event) => { + event.preventDefault(); + const rect = el.getBoundingClientRect(); + const midY = rect.top + rect.height / 2; - el.removeClass("drag-over-top"); - el.removeClass("drag-over-bottom"); + el.removeClass("drag-over-top"); + el.removeClass("drag-over-bottom"); - if (event.clientY < midY) { - el.addClass("drag-over-top"); - } else { - el.addClass("drag-over-bottom"); - } - }; + if (event.clientY < midY) { + el.addClass("drag-over-top"); + } else { + el.addClass("drag-over-bottom"); + } + }; - el.ondragleave = () => { - el.removeClass("drag-over-top"); - el.removeClass("drag-over-bottom"); - }; + el.ondragleave = () => { + el.removeClass("drag-over-top"); + el.removeClass("drag-over-bottom"); + }; - el.ondrop = async (event) => { - event.preventDefault(); - el.removeClass("drag-over-top"); - el.removeClass("drag-over-bottom"); - const oldIndex = parseInt( - event.dataTransfer?.getData("text/plain") || "-1" - ); + el.ondrop = async (event) => { + event.preventDefault(); + el.removeClass("drag-over-top"); + el.removeClass("drag-over-bottom"); + const oldIndex = parseInt( + event.dataTransfer?.getData("text/plain") || "-1" + ); - if (oldIndex >= 0) { - const rect = el.getBoundingClientRect(); - const midY = rect.top + rect.height / 2; - const insertAfter = event.clientY >= midY; + if (oldIndex >= 0) { + const rect = el.getBoundingClientRect(); + const midY = rect.top + rect.height / 2; + const insertAfter = event.clientY >= midY; - let targetIndex = index; - if (insertAfter) targetIndex++; + let targetIndex = index; + if (insertAfter) targetIndex++; - const item = toolbar.commands.splice(oldIndex, 1)[0]; + const item = toolbar.commands.splice(oldIndex, 1)[0]; - if (oldIndex < targetIndex) { - targetIndex--; - } + if (oldIndex < targetIndex) { + targetIndex--; + } - toolbar.commands.splice(targetIndex, 0, item); - await this.plugin.saveSettings(); - this.renderCommandListForToolbar( - container, - toolbar, - toolbarIndex - ); - } - }; - }); - } + toolbar.commands.splice(targetIndex, 0, item); + await this.plugin.saveSettings(); + this.renderCommandListForToolbar(container, toolbar, toolbarIndex); + } + }; + }); + } } diff --git a/src/toolbar-extension.ts b/src/toolbar-extension.ts index 81c3e38..88b28cd 100644 --- a/src/toolbar-extension.ts +++ b/src/toolbar-extension.ts @@ -1,9 +1,9 @@ import { - ViewPlugin, - EditorView, - ViewUpdate, - Decoration, - DecorationSet, + ViewPlugin, + EditorView, + ViewUpdate, + Decoration, + DecorationSet, } from "@codemirror/view"; import { syntaxTree } from "@codemirror/language"; import { App, ButtonComponent } from "obsidian"; @@ -13,289 +13,267 @@ import { ToolbarConfig, ContextBinding, ContextType } from "./settings"; * Creates a CodeMirror 6 ViewPlugin that displays a context-aware toolbar at the bottom * when text is selected or cursor is in a specific context. */ -export function createToolbarExtension( - app: App, - settings: any -) { - return ViewPlugin.fromClass( - class { - decorations: DecorationSet; - tooltip: HTMLElement | null = null; - app: App; - toolbars: ToolbarConfig[]; - contextBindings: ContextBinding[]; - useIcons: boolean; - commandIcons: Record; - enableHapticFeedback: boolean; - editorContainer: HTMLElement | null = null; +export function createToolbarExtension(app: App, settings: any) { + return ViewPlugin.fromClass( + class { + decorations: DecorationSet; + tooltip: HTMLElement | null = null; + app: App; + toolbars: ToolbarConfig[]; + contextBindings: ContextBinding[]; + useIcons: boolean; + commandIcons: Record; + enableHapticFeedback: boolean; + editorContainer: HTMLElement | null = null; - constructor(view: EditorView) { - this.decorations = Decoration.none; - this.app = app; - this.toolbars = settings.toolbars; - this.contextBindings = settings.contextBindings; - this.useIcons = settings.useIcons; - this.commandIcons = settings.commandIcons; - this.enableHapticFeedback = settings.enableHapticFeedback; + constructor(view: EditorView) { + this.decorations = Decoration.none; + this.app = app; + this.toolbars = settings.toolbars; + this.contextBindings = settings.contextBindings; + this.useIcons = settings.useIcons; + this.commandIcons = settings.commandIcons; + this.enableHapticFeedback = settings.enableHapticFeedback; - // Find the editor container to anchor the toolbar - this.editorContainer = this.findEditorContainer(view.dom); + // Find the editor container to anchor the toolbar + this.editorContainer = this.findEditorContainer(view.dom); - this.updateTooltip(view); - } + this.updateTooltip(view); + } - hapticFeedback(duration: number = 10) { - if (this.enableHapticFeedback && navigator.vibrate) { - navigator.vibrate(duration); - } - } + hapticFeedback(duration: number = 10) { + if (this.enableHapticFeedback && navigator.vibrate) { + navigator.vibrate(duration); + } + } - findEditorContainer(element: HTMLElement): HTMLElement | null { - // Find the workspace-leaf-content container - let current = element.parentElement; - while (current) { - if (current.classList.contains("workspace-leaf-content")) { - return current; - } - current = current.parentElement; - } - return null; - } + findEditorContainer(element: HTMLElement): HTMLElement | null { + // Find the workspace-leaf-content container + let current = element.parentElement; + while (current) { + if (current.classList.contains("workspace-leaf-content")) { + return current; + } + current = current.parentElement; + } + return null; + } - update(update: ViewUpdate) { - if ( - update.selectionSet || - update.viewportChanged || - update.docChanged - ) { - // Defer tooltip update to avoid reading layout during update - requestAnimationFrame(() => { - this.updateTooltip(update.view); - }); - } - } + update(update: ViewUpdate) { + if ( + update.selectionSet || + update.viewportChanged || + update.docChanged + ) { + // Defer tooltip update to avoid reading layout during update + requestAnimationFrame(() => { + this.updateTooltip(update.view); + }); + } + } - updateTooltip(view: EditorView) { - const selection = view.state.selection.main; + updateTooltip(view: EditorView) { + const selection = view.state.selection.main; - // Remove existing tooltip if present - if (this.tooltip) { - this.tooltip.remove(); - this.tooltip = null; - } + // Remove existing tooltip if present + if (this.tooltip) { + this.tooltip.remove(); + this.tooltip = null; + } - // Show toolbar if there's a selection or cursor is in specific context - if (!selection.empty || this.hasContext(view, selection.from)) { - this.showTooltip(view); - } - } + // Show toolbar if there's a selection or cursor is in specific context + if (!selection.empty || this.hasContext(view, selection.from)) { + this.showTooltip(view); + } + } - hasContext(view: EditorView, pos: number): boolean { - const activeContexts = this.getMatchingContexts(view, pos); - // Check if any binding matches the current context - for (const binding of this.contextBindings) { - if (activeContexts.has(binding.contextType)) { - return true; - } - } - return false; - } + hasContext(view: EditorView, pos: number): boolean { + const activeContexts = this.getMatchingContexts(view, pos); + // Check if any binding matches the current context + for (const binding of this.contextBindings) { + if (activeContexts.has(binding.contextType)) { + return true; + } + } + return false; + } - getActiveToolbar( - view: EditorView, - pos: number - ): ToolbarConfig | null { - const activeContexts = this.getMatchingContexts(view, pos); - // Collect all matching toolbars and concatenate their commands - const matchingToolbars: ToolbarConfig[] = []; - const seenCommands = new Set(); + getActiveToolbar(view: EditorView, pos: number): ToolbarConfig | null { + const activeContexts = this.getMatchingContexts(view, pos); + // Collect all matching toolbars and concatenate their commands + const matchingToolbars: ToolbarConfig[] = []; + const seenCommands = new Set(); - for (const binding of this.contextBindings) { - if (activeContexts.has(binding.contextType)) { - const toolbar = this.toolbars.find( - (t) => t.id === binding.toolbarId - ); - if (toolbar) { - matchingToolbars.push(toolbar); - } - } - } + for (const binding of this.contextBindings) { + if (activeContexts.has(binding.contextType)) { + const toolbar = this.toolbars.find( + (t) => t.id === binding.toolbarId + ); + if (toolbar) { + matchingToolbars.push(toolbar); + } + } + } - // If no matches, return null - if (matchingToolbars.length === 0) { - return null; - } + // If no matches, return null + if (matchingToolbars.length === 0) { + return null; + } - // Concatenate commands from all matching toolbars, removing duplicates - const combinedCommands: string[] = []; - for (const toolbar of matchingToolbars) { - for (const command of toolbar.commands) { - if (!seenCommands.has(command)) { - seenCommands.add(command); - combinedCommands.push(command); - } - } - } + // Concatenate commands from all matching toolbars, removing duplicates + const combinedCommands: string[] = []; + for (const toolbar of matchingToolbars) { + for (const command of toolbar.commands) { + if (!seenCommands.has(command)) { + seenCommands.add(command); + combinedCommands.push(command); + } + } + } - // Return a virtual toolbar with combined commands - return { - id: "combined", - name: "Combined Toolbar", - commands: combinedCommands, - }; - } + // Return a virtual toolbar with combined commands + return { + id: "combined", + name: "Combined Toolbar", + commands: combinedCommands, + }; + } - getMatchingContexts( - view: EditorView, - pos: number - ): Set { - const contexts = new Set(); - contexts.add("default"); + getMatchingContexts(view: EditorView, pos: number): Set { + const contexts = new Set(); + contexts.add("default"); - if (!view.state.selection.main.empty) { - contexts.add("selection"); - } + if (!view.state.selection.main.empty) { + contexts.add("selection"); + } - syntaxTree(view.state).iterate({ - from: pos, - to: pos, - enter: (node: any) => { - const nodeName = node.type.name; + syntaxTree(view.state).iterate({ + from: pos, + to: pos, + enter: (node: any) => { + const nodeName = node.type.name; - if ( - nodeName === "BulletList" || - nodeName === "OrderedList" || - nodeName.startsWith( - "HyperMD-list-line_HyperMD-list-line-" - ) - ) { - contexts.add("list"); - } + if ( + nodeName === "BulletList" || + nodeName === "OrderedList" || + nodeName.startsWith("HyperMD-list-line_HyperMD-list-line-") + ) { + contexts.add("list"); + } - if ( - nodeName === "Task" || - nodeName.includes("HyperMD-task-line") - ) { - contexts.add("task"); - } + if (nodeName === "Task" || nodeName.includes("HyperMD-task-line")) { + contexts.add("task"); + } - if ( - nodeName.startsWith("ATXHeading") || - nodeName === "SetextHeading" || - nodeName.startsWith("HyperMD-header") - ) { - contexts.add("heading"); - } + if ( + nodeName.startsWith("ATXHeading") || + nodeName === "SetextHeading" || + nodeName.startsWith("HyperMD-header") + ) { + contexts.add("heading"); + } - if ( - nodeName === "FencedCode" || - nodeName === "CodeBlock" || - nodeName.includes("HyperMD-codeblock") - ) { - contexts.add("code-block"); - } + if ( + nodeName === "FencedCode" || + nodeName === "CodeBlock" || + nodeName.includes("HyperMD-codeblock") + ) { + contexts.add("code-block"); + } - if ( - nodeName === "Table" || - nodeName.startsWith("Table") || - nodeName.includes("HyperMD-table") - ) { - contexts.add("table"); - } + if ( + nodeName === "Table" || + nodeName.startsWith("Table") || + nodeName.includes("HyperMD-table") + ) { + contexts.add("table"); + } - if ( - nodeName === "Blockquote" || - nodeName === "QuoteMark" || - nodeName.includes("HyperMD-quote") - ) { - contexts.add("blockquote"); - } + if ( + nodeName === "Blockquote" || + nodeName === "QuoteMark" || + nodeName.includes("HyperMD-quote") + ) { + contexts.add("blockquote"); + } - if ( - nodeName === "Link" || - nodeName.includes("link") || - nodeName.includes("URL") || - nodeName.includes("HyperMD-link") - ) { - contexts.add("link"); - } - }, - }); + if ( + nodeName === "Link" || + nodeName.includes("link") || + nodeName.includes("URL") || + nodeName.includes("HyperMD-link") + ) { + contexts.add("link"); + } + }, + }); - return contexts; - } + return contexts; + } - showTooltip(view: EditorView) { - const selection = view.state.selection.main; + showTooltip(view: EditorView) { + const selection = view.state.selection.main; - // Get the active toolbar based on context - const activeToolbar = this.getActiveToolbar( - view, - selection.from - ); + // Get the active toolbar based on context + const activeToolbar = this.getActiveToolbar(view, selection.from); - if (!activeToolbar || activeToolbar.commands.length === 0) { - return; - } + if (!activeToolbar || activeToolbar.commands.length === 0) { + return; + } - // Create tooltip element - this.tooltip = (this.editorContainer || view.dom).createDiv({ - cls: "mobile-selection-toolbar", - attr: { "data-toolbar-id": activeToolbar.id }, - }); + // Create tooltip element + this.tooltip = (this.editorContainer || view.dom).createDiv({ + cls: "mobile-selection-toolbar", + attr: { "data-toolbar-id": activeToolbar.id }, + }); - // Get all available commands - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const commands = (this.app as any).commands?.commands || {}; + // Get all available commands + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const commands = (this.app as any).commands?.commands || {}; - // Add command buttons - activeToolbar.commands.forEach((commandId) => { - const command = commands[commandId]; - const iconToUse = - this.commandIcons[commandId] || command.icon; - if (command && this.tooltip) { - if (this.useIcons && iconToUse) { - new ButtonComponent(this.tooltip) - /* .setClass("mobile-toolbar-button") */ - .setIcon(iconToUse) - .setTooltip(command.name || commandId) - .onClick((e) => { - // Haptic feedback on button click - this.hapticFeedback(10); - // Execute the command - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ( - this.app as any - ).commands?.executeCommandById(commandId); - }); - } else { - new ButtonComponent(this.tooltip) - /* .setClass("mobile-toolbar-button") */ - .setButtonText(command.name || commandId) - .setTooltip(command.name || commandId) - .onClick((e) => { - // Haptic feedback on button click - this.hapticFeedback(10); - // Execute the command - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ( - this.app as any - ).commands?.executeCommandById(commandId); - }); - } - } - }); - } + // Add command buttons + activeToolbar.commands.forEach((commandId) => { + const command = commands[commandId]; + const iconToUse = this.commandIcons[commandId] || command.icon; + if (command && this.tooltip) { + if (this.useIcons && iconToUse) { + new ButtonComponent(this.tooltip) + /* .setClass("mobile-toolbar-button") */ + .setIcon(iconToUse) + .setTooltip(command.name || commandId) + .onClick((e) => { + // Haptic feedback on button click + this.hapticFeedback(10); + // Execute the command + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.app as any).commands?.executeCommandById(commandId); + }); + } else { + new ButtonComponent(this.tooltip) + /* .setClass("mobile-toolbar-button") */ + .setButtonText(command.name || commandId) + .setTooltip(command.name || commandId) + .onClick((e) => { + // Haptic feedback on button click + this.hapticFeedback(10); + // Execute the command + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.app as any).commands?.executeCommandById(commandId); + }); + } + } + }); + } - destroy() { - if (this.tooltip) { - this.tooltip.remove(); - this.tooltip = null; - } - } - }, - { - // No decorations needed for this plugin - } - ); + destroy() { + if (this.tooltip) { + this.tooltip.remove(); + this.tooltip = null; + } + } + }, + { + // No decorations needed for this plugin + } + ); } diff --git a/styles.css b/styles.css index 58aca77..1eee6be 100644 --- a/styles.css +++ b/styles.css @@ -5,134 +5,134 @@ Includes FAB and selection toolbar styles with mobile-friendly design /* Ensure workspace-leaf-content is positioned for absolute children */ .workspace-leaf-content { - position: relative; + position: relative; } /* Floating Action Button (FAB) */ .mobile-fab { - position: absolute; - bottom: calc(20px + env(safe-area-inset-bottom)); - right: 20px; - width: 56px; - height: 56px; - border-radius: 50%; - background-color: var(--interactive-accent); - color: var(--text-on-accent); - border: none; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - z-index: 100; - transition: transform 0.2s ease, box-shadow 0.2s ease; + position: absolute; + bottom: calc(20px + env(safe-area-inset-bottom)); + right: 20px; + width: 56px; + height: 56px; + border-radius: 50%; + background-color: var(--interactive-accent); + color: var(--text-on-accent); + border: none; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + z-index: 100; + transition: transform 0.2s ease, box-shadow 0.2s ease; } .mobile-fab:hover { - transform: scale(1.05); - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4); + transform: scale(1.05); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4); } .mobile-fab:active { - transform: scale(0.95); + transform: scale(0.95); } .mobile-fab svg { - width: 24px; - height: 24px; + width: 24px; + height: 24px; } /* Selection Toolbar */ .mobile-selection-toolbar { - display: flex; - gap: 8px; - padding: 8px; - background-color: var(--background-primary); - border: 1px solid var(--background-modifier-border); - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - position: absolute; - bottom: calc(10px + env(safe-area-inset-bottom)); - left: 50%; - transform: translateX(-50%); - z-index: 1000; + display: flex; + gap: 8px; + padding: 8px; + background-color: var(--background-primary); + border: 1px solid var(--background-modifier-border); + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + position: absolute; + bottom: calc(10px + env(safe-area-inset-bottom)); + left: 50%; + transform: translateX(-50%); + z-index: 1000; } .mobile-toolbar-button { - padding: 6px 12px; - background-color: var(--interactive-normal); - color: var(--text-normal); - border: 1px solid var(--background-modifier-border); - border-radius: 4px; - cursor: pointer; - font-size: 14px; - font-weight: 500; - transition: background-color 0.2s ease; - display: flex; - align-items: center; - justify-content: center; - min-width: 36px; - min-height: 36px; + padding: 6px 12px; + background-color: var(--interactive-normal); + color: var(--text-normal); + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: background-color 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + min-width: 36px; + min-height: 36px; } .mobile-toolbar-button:hover { - background-color: var(--interactive-hover); + background-color: var(--interactive-hover); } .mobile-toolbar-button:active { - background-color: var(--interactive-accent); - color: var(--text-on-accent); + background-color: var(--interactive-accent); + color: var(--text-on-accent); } .mobile-toolbar-icon { - display: flex; - align-items: center; - justify-content: center; + display: flex; + align-items: center; + justify-content: center; } .mobile-toolbar-icon svg { - width: 18px; - height: 18px; + width: 18px; + height: 18px; } /* Settings Page Styles */ .mobile-toolbar-section { - margin: 1.5em 0; - padding: 1em; - background-color: var(--background-secondary); - border-radius: 8px; - border: 1px solid var(--background-modifier-border); + margin: 1.5em 0; + padding: 1em; + background-color: var(--background-secondary); + border-radius: 8px; + border: 1px solid var(--background-modifier-border); } .mobile-toolbar-header { - font-weight: 600; - border-bottom: 1px solid var(--background-modifier-border); - padding-bottom: 0.5em; - margin-bottom: 0.5em; + font-weight: 600; + border-bottom: 1px solid var(--background-modifier-border); + padding-bottom: 0.5em; + margin-bottom: 0.5em; } .mobile-command-list { - margin: 0.5em 0; + margin: 0.5em 0; } .mobile-add-command-btn { - margin-top: 0.5em; + margin-top: 0.5em; } /* Settings Drag and Drop */ .mobile-plugin-draggable-item { - cursor: grab; + cursor: grab; } .mobile-plugin-draggable-item.is-dragging { - opacity: 0.5; - cursor: grabbing; + opacity: 0.5; + cursor: grabbing; } .mobile-plugin-draggable-item.drag-over-top { - border-top: 2px solid var(--interactive-accent); + border-top: 2px solid var(--interactive-accent); } .mobile-plugin-draggable-item.drag-over-bottom { - border-bottom: 2px solid var(--interactive-accent); + border-bottom: 2px solid var(--interactive-accent); }