diff --git a/README.md b/README.md index 17e392f..63ad2a5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Obsidian Copy Inline Code Plugin -This plugin for [Obsidian](https://obsidian.md) adds an icon inside each inline code, which when clicked, copies the content of the code into the clipboard. See screenshot of the functionality below. +This plugin for [Obsidian](https://obsidian.md) adds a customizable icon inside each inline code, which when clicked, copies the content of the code into the clipboard. See screenshot of the functionality below. ![Screenshot of the copy inline code plugin](plugin-screenshot.png) diff --git a/plugin-screenshot.png b/plugin-screenshot.png index 0317652..274d97f 100644 Binary files a/plugin-screenshot.png and b/plugin-screenshot.png differ diff --git a/src/copy-code-widget.ts b/src/copy-code-widget.ts index 17f9c06..512c953 100644 --- a/src/copy-code-widget.ts +++ b/src/copy-code-widget.ts @@ -1,15 +1,31 @@ import { EditorView, WidgetType } from "@codemirror/view"; -import { Notice } from "obsidian"; +import { Notice, getIcon } from "obsidian"; export class CopyWidget extends WidgetType { showOnHover: boolean; - constructor(showOnHover: boolean) { + iconName: string; + useLegacyIcon: boolean; + constructor(showOnHover: boolean, iconName: string, useLegacyIcon: boolean) { super(); this.showOnHover = showOnHover; + this.iconName = iconName; + this.useLegacyIcon = useLegacyIcon; } toDOM(view: EditorView): HTMLElement { - const icon = createSpan({cls: "copy-to-clipboard-icon", text: "\xa0📋"}) + const icon = createSpan({cls: "copy-to-clipboard-icon"}); + + if (this.useLegacyIcon) { + icon.setText("\xa0📋"); + } else { + const lucideIcon = getIcon(this.iconName); + if (lucideIcon) { + icon.appendChild(lucideIcon); + } else { + icon.setText("\xa0📋"); + } + } + icon.toggleClass("show-on-hover", this.showOnHover) icon.onclick = (event) => { const element = (event.target as HTMLElement) diff --git a/src/copy-inline-code-view-plugin.ts b/src/copy-inline-code-view-plugin.ts index 17ca492..f89dd0c 100644 --- a/src/copy-inline-code-view-plugin.ts +++ b/src/copy-inline-code-view-plugin.ts @@ -15,9 +15,19 @@ class CopyInlineCodeViewPlugin implements PluginValue { decorations: DecorationSet; showOnHover: boolean; filters: RegexFilters; - constructor(view: EditorView, showOnHover: boolean, filters: RegexFilters) { + iconName: string; + useLegacyIcon: boolean; + constructor( + view: EditorView, + showOnHover: boolean, + filters: RegexFilters, + iconName: string, + useLegacyIcon: boolean + ) { this.showOnHover = showOnHover; this.filters = filters; + this.iconName = iconName; + this.useLegacyIcon = useLegacyIcon; this.decorations = this.buildDecorations(view); } @@ -33,7 +43,9 @@ class CopyInlineCodeViewPlugin implements PluginValue { buildDecorations(view: EditorView): DecorationSet { const builder = new RangeSetBuilder(); const showOnHover = this.showOnHover; - const filters = this.filters; + const filters = this.filters; + const iconName = this.iconName; + const useLegacyIcon = this.useLegacyIcon; for (const { from, to } of view.visibleRanges) { syntaxTree(view.state).iterate({ @@ -52,7 +64,11 @@ class CopyInlineCodeViewPlugin implements PluginValue { node.to + 1, node.to + 1, Decoration.widget({ - widget: new CopyWidget(showOnHover), + widget: new CopyWidget( + showOnHover, + iconName, + useLegacyIcon + ), }) ); } @@ -66,11 +82,19 @@ class CopyInlineCodeViewPlugin implements PluginValue { export const createCopyPlugin = ( showOnHover: boolean, - filters: RegexFilters + filters: RegexFilters, + iconName: string, + useLegacyIcon: boolean ) => { return ViewPlugin.define( (view: EditorView) => - new CopyInlineCodeViewPlugin(view, showOnHover, filters), + new CopyInlineCodeViewPlugin( + view, + showOnHover, + filters, + iconName, + useLegacyIcon + ), { decorations: (p) => p.decorations, } diff --git a/src/main.ts b/src/main.ts index c69856b..f9e13d1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,16 +1,20 @@ import { CopyInlineCodePluginTab } from "./settings"; -import { Notice, Plugin } from "obsidian"; +import { Notice, Plugin, getIcon } from "obsidian"; import { createCopyPlugin } from "./copy-inline-code-view-plugin"; import { RegexFilters, shouldExclude } from "./regex-exclude"; interface CopyInlineCodePluginSettings { showOnHover: boolean; regexFilters: RegexFilters; + iconName: string; + useLegacyIcon: boolean; } const DEFAULT_SETTINGS: Partial = { showOnHover: false, regexFilters: [], + iconName: "lucide-copy", + useLegacyIcon: false, }; export default class CopyInlineCodePlugin extends Plugin { @@ -38,7 +42,9 @@ export default class CopyInlineCodePlugin extends Plugin { this.registerEditorExtension([ createCopyPlugin( this.settings.showOnHover, - this.settings.regexFilters + this.settings.regexFilters, + this.settings.iconName, + this.settings.useLegacyIcon ), ]); this.registerMarkdownPostProcessor((element, context) => { @@ -59,9 +65,20 @@ export default class CopyInlineCodePlugin extends Plugin { } const icon = createSpan({ - cls: "copy-to-clipboard-icon", - text: "\xa0📋", + cls: "copy-to-clipboard-icon icon-margin-left", }); + + if (this.settings.useLegacyIcon) { + icon.setText("\xa0📋"); + } else { + const lucideIcon = getIcon(this.settings.iconName); + if (lucideIcon) { + icon.appendChild(lucideIcon); + } else { + icon.setText("\xa0📋"); + } + } + icon.toggleClass("show-on-hover", this.settings.showOnHover); icon.onclick = (event) => { diff --git a/src/settings.ts b/src/settings.ts index bfbd054..5378866 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,5 +1,5 @@ import CopyInlineCodePlugin from "./main"; -import { App, PluginSettingTab, Setting } from "obsidian"; +import { App, PluginSettingTab, Setting, getIconIds, getIcon } from "obsidian"; export class CopyInlineCodePluginTab extends PluginSettingTab { plugin: CopyInlineCodePlugin; @@ -15,7 +15,7 @@ export class CopyInlineCodePluginTab extends PluginSettingTab { containerEl.empty(); containerEl.createEl("p", { cls: "tasks-setting-important", - text: "Changing any settings requires a restart of obsidian.", + text: "Changing any settings requires a restart of obsidian or use of the 'Reload app without saving' command.", }); new Setting(containerEl) @@ -46,6 +46,122 @@ export class CopyInlineCodePluginTab extends PluginSettingTab { this.renderRegexList(regexListContainer); }); }); + + // Icon selection setting + containerEl.createEl("h3", { text: "Icon Settings" }); + + // Only show Lucide icon settings if legacy mode is disabled + if (!this.plugin.settings.useLegacyIcon) { + const iconDesc = containerEl.createDiv(); + iconDesc.createEl("p", { + text: "Choose a Lucide icon for the copy button. You can browse available icons at: ", + }); + const iconLink = iconDesc.createEl("a", { + text: "https://lucide.dev/icons/", + href: "https://lucide.dev/icons/", + }); + iconLink.setAttribute("target", "_blank"); + + iconDesc.createEl("p", { + text: "Copy the icon name (e.g., 'copy', 'clipboard', ...) and paste it below.", + }); + + const iconSetting = new Setting(containerEl) + .setName("Icon name") + .setDesc( + "Lucide icon name" + ); + + // Create a container for the input and preview + const inputContainer = iconSetting.controlEl.createDiv({ + cls: "icon-input-container", + }); + + // Create preview element + const iconPreview = inputContainer.createSpan(); + + // Function to update the icon preview + const updateIconPreview = (iconName: string) => { + iconPreview.empty(); + + let lucideIcon: SVGSVGElement | null = null; + let isInvalid = false; + + if (!iconName) { + isInvalid = true; + lucideIcon = getIcon("lucide-x"); + } else { + lucideIcon = getIcon(iconName); + if (!lucideIcon) { + isInvalid = true; + lucideIcon = getIcon("lucide-x"); + } + } + + if (lucideIcon) { + lucideIcon.classList.add("preview-icon"); + if (isInvalid) { + lucideIcon.classList.add("invalid-preview-icon"); + } + iconPreview.appendChild(lucideIcon); + } + }; + + // Create text input + const textInput = inputContainer.createEl("input", { + type: "text", + cls: "text-input", + }); + textInput.style.flex = "1"; + textInput.placeholder = "copy"; + + // Get the base icon name (without lucide- prefix) for display + const baseIconName = this.plugin.settings.iconName.startsWith( + "lucide-" + ) + ? this.plugin.settings.iconName.substring(7) + : this.plugin.settings.iconName; + + textInput.value = baseIconName; + + // Initialize preview with current icon + updateIconPreview(this.plugin.settings.iconName); + + textInput.addEventListener("input", async (event) => { + const target = event.target as HTMLInputElement; + const value = target.value; + const trimmedValue = `lucide-${value.trim()}`; + const availableIcons = getIconIds(); + + // Update preview in real-time + updateIconPreview(trimmedValue); + + // Validate icon exists + if (trimmedValue && !availableIcons.includes(trimmedValue)) { + textInput.classList.add("regex-input-error"); + return; + } + + textInput.classList.remove("regex-input-error"); + this.plugin.settings.iconName = trimmedValue; + await this.plugin.saveSettings(); + }); + } + + new Setting(containerEl) + .setName("Use legacy icon") + .setDesc( + "Use the original clipboard emoji (📋) instead of Lucide icons" + ) + .addToggle((component) => { + component + .setValue(this.plugin.settings.useLegacyIcon) + .onChange(async (value) => { + this.plugin.settings.useLegacyIcon = value; + await this.plugin.saveSettings(); + this.display(); + }); + }); } private renderRegexList(container: HTMLElement) { diff --git a/styles.css b/styles.css index d331f06..04ee014 100644 --- a/styles.css +++ b/styles.css @@ -2,6 +2,15 @@ cursor: pointer; } +.copy-to-clipboard-icon svg { + vertical-align: text-bottom; + pointer-events: none; +} + +.icon-margin-left { + margin-left: 0.25em; +} + .show-on-hover { cursor: pointer; opacity: 0; @@ -24,3 +33,21 @@ code:hover .show-on-hover { .regex-input-error { border-color: var(--text-error) !important; } + +.icon-input-container { + width: 100%; + display: flex; + align-items: center; + gap: 10px; +} + +.preview-icon { + color: var(--text-normal); + display: flex; + align-items: center; + justify-content: center; +} + +.invalid-preview-icon { + color: var(--text-error) !important; +} \ No newline at end of file