Merge pull request #20 from ozavodny/feat/customizable-icon

feat(#11): customizable icon
This commit is contained in:
Ondřej Závodný 2025-05-30 17:03:48 +02:00 committed by GitHub
commit 0a36d04bbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 215 additions and 15 deletions

View file

@ -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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 541 KiB

View file

@ -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)

View file

@ -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<Decoration>();
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,
}

View file

@ -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<CopyInlineCodePluginSettings> = {
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) => {

View file

@ -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) {

View file

@ -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;
}