mirror of
https://github.com/ozavodny/obsidian-copy-inline-code-plugin.git
synced 2026-07-22 08:10:25 +00:00
feat(#11): customizable icon
This commit is contained in:
parent
dde5cd3b22
commit
b1ba0c39a7
7 changed files with 215 additions and 15 deletions
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 541 KiB |
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
25
src/main.ts
25
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<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) => {
|
||||
|
|
|
|||
120
src/settings.ts
120
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) {
|
||||
|
|
|
|||
27
styles.css
27
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;
|
||||
}
|
||||
Loading…
Reference in a new issue