feat(ckeditor): add copy button for inline code

This commit is contained in:
Elian Doran
2026-04-11 00:36:43 +03:00
parent adbe8f6c42
commit 147ecbccda
3 changed files with 159 additions and 16 deletions

View File

@@ -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,
];
/**

View File

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

View File

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