diff --git a/packages/ckeditor5/src/plugins.ts b/packages/ckeditor5/src/plugins.ts index abf51f0869..29ca454485 100644 --- a/packages/ckeditor5/src/plugins.ts +++ b/packages/ckeditor5/src/plugins.ts @@ -31,6 +31,7 @@ import CodeBlockLanguageDropdown from "./plugins/code_block_language_dropdown.js import MoveBlockUpDownPlugin from "./plugins/move_block_updown.js"; import ScrollOnUndoRedoPlugin from "./plugins/scroll_on_undo_redo.js" import InlineCodeNoSpellcheck from "./plugins/inline_code_no_spellcheck.js"; +import InlineCodeToolbar from "./plugins/inline_code_toolbar.js"; /** * Plugins that are specific to Trilium and not part of the CKEditor 5 core, included in both text editors but not in the attribute editor. @@ -53,6 +54,7 @@ const TRILIUM_PLUGINS: typeof Plugin[] = [ MoveBlockUpDownPlugin, ScrollOnUndoRedoPlugin, InlineCodeNoSpellcheck, + InlineCodeToolbar, ]; /** diff --git a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts index 836549fb1c..9bea57e5b3 100644 --- a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts +++ b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts @@ -38,28 +38,43 @@ export class CopyToClipboardCommand extends Command { this.executeCallback = this.editor.config.get("clipboard")?.copy; } + // Try code block first const codeBlockEl = selection.getFirstPosition()?.findAncestor("codeBlock"); - if (!codeBlockEl) { - console.warn("Unable to find code block element to copy from."); + if (codeBlockEl) { + const codeText = Array.from(codeBlockEl.getChildren()) + .map(child => "data" in child ? child.data : "\n") + .join(""); + this.copyText(codeText, "code block"); return; } - const codeText = Array.from(codeBlockEl.getChildren()) - .map(child => "data" in child ? child.data : "\n") - .join(""); - - if (codeText) { - if (!this.executeCallback) { - navigator.clipboard.writeText(codeText).then(() => { - console.log('Code block copied to clipboard'); - }).catch(err => { - console.error('Failed to copy code block', err); - }); - } else { - this.executeCallback(codeText); + // Try inline code (text with 'code' attribute) + const position = selection.getFirstPosition(); + if (position) { + const textNode = position.textNode || position.nodeBefore || position.nodeAfter; + if (textNode && "data" in textNode && textNode.hasAttribute?.("code")) { + this.copyText(textNode.data as string, "inline code"); + return; } + } + + console.warn("No code block or inline code found to copy from."); + } + + private copyText(text: string, source: string) { + if (!text) { + console.warn(`No text found in ${source}.`); + return; + } + + if (!this.executeCallback) { + navigator.clipboard.writeText(text).then(() => { + console.log(`${source} copied to clipboard`); + }).catch(err => { + console.error(`Failed to copy ${source}`, err); + }); } else { - console.warn('No code block selected or found.'); + this.executeCallback(text); } } diff --git a/packages/ckeditor5/src/plugins/inline_code_toolbar.ts b/packages/ckeditor5/src/plugins/inline_code_toolbar.ts new file mode 100644 index 0000000000..750bd158c4 --- /dev/null +++ b/packages/ckeditor5/src/plugins/inline_code_toolbar.ts @@ -0,0 +1,126 @@ +import { BalloonPanelView, ButtonView, Plugin, ToolbarView } from "ckeditor5"; +import CopyToClipboardButton from "./copy_to_clipboard_button"; +import copyIcon from "../icons/copy.svg?raw"; + +/** + * Shows a small toolbar with a copy button when the cursor is on inline code. + */ +export default class InlineCodeToolbar extends Plugin { + + static get requires() { + return [CopyToClipboardButton] as const; + } + + private balloon?: BalloonPanelView; + private toolbar?: ToolbarView; + + init() { + const editor = this.editor; + + // Create toolbar with copy button + this.toolbar = new ToolbarView(editor.locale); + const copyButton = new ButtonView(editor.locale); + copyButton.set({ + icon: copyIcon, + tooltip: "Copy to clipboard" + }); + copyButton.on("execute", () => { + editor.execute("copyToClipboard"); + this.hideToolbar(); + }); + this.toolbar.items.add(copyButton); + + // Create balloon panel + this.balloon = new BalloonPanelView(editor.locale); + this.balloon.content.add(this.toolbar); + this.balloon.class = "ck-toolbar-container"; + + editor.ui.view.body.add(this.balloon); + + // Show/hide based on selection + this.listenTo(editor.model.document.selection, "change:range", () => { + this.updateToolbarVisibility(); + }); + + // Hide on editor blur + this.listenTo(editor.ui.focusTracker, "change:isFocused", (_evt, _name, isFocused) => { + if (!isFocused) { + this.hideToolbar(); + } + }); + } + + private updateToolbarVisibility() { + const editor = this.editor; + const selection = editor.model.document.selection; + const position = selection.getFirstPosition(); + + // Don't show for code blocks (they have their own toolbar) + if (position?.findAncestor("codeBlock")) { + this.hideToolbar(); + return; + } + + // Check if cursor is on inline code + const textNode = position?.textNode; + if (textNode?.hasAttribute("code")) { + this.showToolbar(textNode); + } else { + this.hideToolbar(); + } + } + + private showToolbar(textNode: unknown) { + if (!this.balloon) return; + + const editor = this.editor; + const view = editor.editing.view; + const mapper = editor.editing.mapper; + + // Map model text node to view element + const viewRange = mapper.toViewRange(editor.model.createRangeOn(textNode as any)); + const viewElement = viewRange.getContainedElement(); + + if (!viewElement) { + this.hideToolbar(); + return; + } + + const domElement = view.domConverter.mapViewToDom(viewElement); + if (!domElement || !(domElement instanceof HTMLElement)) { + this.hideToolbar(); + return; + } + + const rect = domElement.getBoundingClientRect(); + this.balloon.pin({ + target: { + top: rect.top, + bottom: rect.bottom, + left: rect.left, + right: rect.right, + width: rect.width, + height: rect.height + } + }); + this.balloon.isVisible = true; + } + + private hideToolbar() { + if (this.balloon) { + this.balloon.isVisible = false; + this.balloon.unpin(); + } + } + + override destroy() { + super.destroy(); + if (this.balloon) { + this.balloon.destroy(); + } + if (this.toolbar) { + this.toolbar.destroy(); + } + } + +}