mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-29 19:12:27 +08:00
Merge pull request #2023 from TriliumNext/feature/code_block_toolbar
Code block toolbar
This commit is contained in:
commit
97b3268429
@ -9,7 +9,7 @@ import treeService from "./tree.js";
|
|||||||
import FNote from "../entities/fnote.js";
|
import FNote from "../entities/fnote.js";
|
||||||
import FAttachment from "../entities/fattachment.js";
|
import FAttachment from "../entities/fattachment.js";
|
||||||
import imageContextMenuService from "../menus/image_context_menu.js";
|
import imageContextMenuService from "../menus/image_context_menu.js";
|
||||||
import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js";
|
import { applySingleBlockSyntaxHighlight, formatCodeBlocks } from "./syntax_highlight.js";
|
||||||
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
||||||
import renderDoc from "./doc_renderer.js";
|
import renderDoc from "./doc_renderer.js";
|
||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
@ -106,7 +106,7 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HT
|
|||||||
await linkService.loadReferenceLinkTitle($(el));
|
await linkService.loadReferenceLinkTitle($(el));
|
||||||
}
|
}
|
||||||
|
|
||||||
await applySyntaxHighlight($renderedContent);
|
await formatCodeBlocks($renderedContent);
|
||||||
} else if (note instanceof FNote) {
|
} else if (note instanceof FNote) {
|
||||||
await renderChildrenList($renderedContent, note);
|
await renderChildrenList($renderedContent, note);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type FNote from "../entities/fnote.js";
|
import type FNote from "../entities/fnote.js";
|
||||||
import { getCurrentLanguage } from "./i18n.js";
|
import { getCurrentLanguage } from "./i18n.js";
|
||||||
import { applySyntaxHighlight } from "./syntax_highlight.js";
|
import { formatCodeBlocks } from "./syntax_highlight.js";
|
||||||
|
|
||||||
export default function renderDoc(note: FNote) {
|
export default function renderDoc(note: FNote) {
|
||||||
return new Promise<JQuery<HTMLElement>>((resolve) => {
|
return new Promise<JQuery<HTMLElement>>((resolve) => {
|
||||||
@ -41,7 +41,7 @@ function processContent(url: string, $content: JQuery<HTMLElement>) {
|
|||||||
$img.attr("src", dir + "/" + $img.attr("src"));
|
$img.attr("src", dir + "/" + $img.attr("src"));
|
||||||
});
|
});
|
||||||
|
|
||||||
applySyntaxHighlight($content);
|
formatCodeBlocks($content);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUrl(docNameValue: string, language: string) {
|
function getUrl(docNameValue: string, language: string) {
|
||||||
|
@ -1,20 +1,22 @@
|
|||||||
import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes } from "@triliumnext/highlightjs";
|
import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes, type AutoHighlightResult, type HighlightResult, type Theme } from "@triliumnext/highlightjs";
|
||||||
import mime_types from "./mime_types.js";
|
import mime_types from "./mime_types.js";
|
||||||
import options from "./options.js";
|
import options from "./options.js";
|
||||||
|
import toast from "./toast.js";
|
||||||
|
import { t } from "./i18n.js";
|
||||||
|
|
||||||
let highlightingLoaded = false;
|
let highlightingLoaded = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks.
|
* Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks.
|
||||||
|
* Additionally, adds a "Copy to clipboard" button.
|
||||||
*
|
*
|
||||||
* @param $container the container under which to look for code blocks and to apply syntax highlighting to them.
|
* @param $container the container under which to look for code blocks and to apply syntax highlighting to them.
|
||||||
*/
|
*/
|
||||||
export async function applySyntaxHighlight($container: JQuery<HTMLElement>) {
|
export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
|
||||||
if (!isSyntaxHighlightEnabled()) {
|
const syntaxHighlightingEnabled = isSyntaxHighlightEnabled();
|
||||||
return;
|
if (syntaxHighlightingEnabled) {
|
||||||
}
|
|
||||||
|
|
||||||
await ensureMimeTypesForHighlighting();
|
await ensureMimeTypesForHighlighting();
|
||||||
|
}
|
||||||
|
|
||||||
const codeBlocks = $container.find("pre code");
|
const codeBlocks = $container.find("pre code");
|
||||||
for (const codeBlock of codeBlocks) {
|
for (const codeBlock of codeBlocks) {
|
||||||
@ -23,8 +25,29 @@ export async function applySyntaxHighlight($container: JQuery<HTMLElement>) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyCopyToClipboardButton($(codeBlock));
|
||||||
|
|
||||||
|
if (syntaxHighlightingEnabled) {
|
||||||
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
|
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
|
||||||
|
const $copyButton = $("<button>")
|
||||||
|
.addClass("bx component btn tn-tool-button bx-copy copy-button")
|
||||||
|
.attr("title", t("code_block.copy_title"))
|
||||||
|
.on("click", () => {
|
||||||
|
const text = $codeBlock.text();
|
||||||
|
|
||||||
|
try {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
toast.showMessage(t("code_block.copy_success"));
|
||||||
|
} catch (e) {
|
||||||
|
toast.showError(t("code_block.copy_failed"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$codeBlock.parent().append($copyButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,7 +57,7 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLEle
|
|||||||
$codeBlock.parent().toggleClass("hljs");
|
$codeBlock.parent().toggleClass("hljs");
|
||||||
const text = $codeBlock.text();
|
const text = $codeBlock.text();
|
||||||
|
|
||||||
let highlightedText = null;
|
let highlightedText: HighlightResult | AutoHighlightResult | null = null;
|
||||||
if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) {
|
if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) {
|
||||||
await ensureMimeTypesForHighlighting();
|
await ensureMimeTypesForHighlighting();
|
||||||
highlightedText = highlightAuto(text);
|
highlightedText = highlightAuto(text);
|
||||||
@ -66,7 +89,7 @@ export async function ensureMimeTypesForHighlighting() {
|
|||||||
|
|
||||||
export function loadHighlightingTheme(themeName: string) {
|
export function loadHighlightingTheme(themeName: string) {
|
||||||
const themePrefix = "default:";
|
const themePrefix = "default:";
|
||||||
let theme = null;
|
let theme: Theme | null = null;
|
||||||
if (themeName.includes(themePrefix)) {
|
if (themeName.includes(themePrefix)) {
|
||||||
theme = Themes[themeName.substring(themePrefix.length)];
|
theme = Themes[themeName.substring(themePrefix.length)];
|
||||||
}
|
}
|
||||||
|
@ -529,6 +529,26 @@ pre:not(.hljs) {
|
|||||||
font-size: 100%;
|
font-size: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > button.copy-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 1em;
|
||||||
|
right: 1em;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > button.copy-button:hover {
|
||||||
|
color: inherit !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > button.copy-button:active {
|
||||||
|
background-color: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
.pointer {
|
.pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
@ -1830,7 +1830,10 @@
|
|||||||
"word_wrapping": "Word wrapping",
|
"word_wrapping": "Word wrapping",
|
||||||
"theme_none": "No syntax highlighting",
|
"theme_none": "No syntax highlighting",
|
||||||
"theme_group_light": "Light themes",
|
"theme_group_light": "Light themes",
|
||||||
"theme_group_dark": "Dark themes"
|
"theme_group_dark": "Dark themes",
|
||||||
|
"copy_success": "The code block was copied to clipboard.",
|
||||||
|
"copy_failed": "The code block could not be copied to the clipboard due to lack of permissions.",
|
||||||
|
"copy_title": "Copy to clipboard"
|
||||||
},
|
},
|
||||||
"classic_editor_toolbar": {
|
"classic_editor_toolbar": {
|
||||||
"title": "Formatting"
|
"title": "Formatting"
|
||||||
|
@ -12,7 +12,7 @@ import { createChatSession, checkSessionExists, setupStreamingResponse, getDirec
|
|||||||
import { extractInChatToolSteps } from "./message_processor.js";
|
import { extractInChatToolSteps } from "./message_processor.js";
|
||||||
import { validateEmbeddingProviders } from "./validation.js";
|
import { validateEmbeddingProviders } from "./validation.js";
|
||||||
import type { MessageData, ToolExecutionStep, ChatData } from "./types.js";
|
import type { MessageData, ToolExecutionStep, ChatData } from "./types.js";
|
||||||
import { applySyntaxHighlight } from "../../services/syntax_highlight.js";
|
import { formatCodeBlocks } from "../../services/syntax_highlight.js";
|
||||||
|
|
||||||
import "../../stylesheets/llm_chat.css";
|
import "../../stylesheets/llm_chat.css";
|
||||||
|
|
||||||
@ -925,7 +925,7 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
|
|
||||||
// Apply syntax highlighting if this is the final update
|
// Apply syntax highlighting if this is the final update
|
||||||
if (isDone) {
|
if (isDone) {
|
||||||
applySyntaxHighlight($(assistantMessageEl as HTMLElement));
|
formatCodeBlocks($(assistantMessageEl as HTMLElement));
|
||||||
|
|
||||||
// Update message in the data model for storage
|
// Update message in the data model for storage
|
||||||
// Find the last assistant message to update, or add a new one if none exists
|
// Find the last assistant message to update, or add a new one if none exists
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* Utility functions for LLM Chat
|
* Utility functions for LLM Chat
|
||||||
*/
|
*/
|
||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
import { applySyntaxHighlight } from "../../services/syntax_highlight.js";
|
import { formatCodeBlocks } from "../../services/syntax_highlight.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format markdown content for display
|
* Format markdown content for display
|
||||||
@ -62,7 +62,7 @@ export function escapeHtml(text: string): string {
|
|||||||
* Apply syntax highlighting to content
|
* Apply syntax highlighting to content
|
||||||
*/
|
*/
|
||||||
export function applyHighlighting(element: HTMLElement): void {
|
export function applyHighlighting(element: HTMLElement): void {
|
||||||
applySyntaxHighlight($(element));
|
formatCodeBlocks($(element));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
|
import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
|
||||||
import { applySyntaxHighlight } from "../../services/syntax_highlight.js";
|
import { formatCodeBlocks } from "../../services/syntax_highlight.js";
|
||||||
import type FNote from "../../entities/fnote.js";
|
import type FNote from "../../entities/fnote.js";
|
||||||
import type { CommandListenerData, EventData } from "../../components/app_context.js";
|
import type { CommandListenerData, EventData } from "../../components/app_context.js";
|
||||||
import { getLocaleById } from "../../services/i18n.js";
|
import { getLocaleById } from "../../services/i18n.js";
|
||||||
@ -125,7 +125,7 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.#applyInlineMermaid();
|
await this.#applyInlineMermaid();
|
||||||
await applySyntaxHighlight(this.$content);
|
await formatCodeBlocks(this.$content);
|
||||||
}
|
}
|
||||||
|
|
||||||
async #applyInlineMermaid() {
|
async #applyInlineMermaid() {
|
||||||
|
@ -49,6 +49,9 @@
|
|||||||
* Backend log: disable some editor features in order to increase performance for large logs (syntax highlighting, folding, etc.).
|
* Backend log: disable some editor features in order to increase performance for large logs (syntax highlighting, folding, etc.).
|
||||||
* [Collapsible table of contents](https://github.com/TriliumNext/Notes/pull/1954) by @SiriusXT
|
* [Collapsible table of contents](https://github.com/TriliumNext/Notes/pull/1954) by @SiriusXT
|
||||||
* Sessions (logins) are no longer stored as files in the data directory, but as entries in the database. This improves the session reliability on Windows platforms.
|
* Sessions (logins) are no longer stored as files in the data directory, but as entries in the database. This improves the session reliability on Windows platforms.
|
||||||
|
* Code blocks in text notes:
|
||||||
|
* For editable notes, clicking on a code block will reveal a toolbar with a way to easily change the programming language and another button to copy the text to clipboard.
|
||||||
|
* For read-only notes, a floating button allows copying the code snippet to clipboard.
|
||||||
|
|
||||||
## 📖 Documentation
|
## 📖 Documentation
|
||||||
|
|
||||||
|
1
packages/ckeditor5/src/icons/copy.svg
Normal file
1
packages/ckeditor5/src/icons/copy.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="fill: rgba(0, 0, 0, 1);transform: ;msFilter:;"><path d="M20 2H10c-1.103 0-2 .897-2 2v4H4c-1.103 0-2 .897-2 2v10c0 1.103.897 2 2 2h10c1.103 0 2-.897 2-2v-4h4c1.103 0 2-.897 2-2V4c0-1.103-.897-2-2-2zM4 20V10h10l.002 10H4zm16-6h-4v-4c0-1.103-.897-2-2-2h-4V4h10v10z"></path></svg>
|
After Width: | Height: | Size: 366 B |
@ -1,4 +1,5 @@
|
|||||||
import "ckeditor5/ckeditor5.css";
|
import "ckeditor5/ckeditor5.css";
|
||||||
|
import "./theme/code_block_toolbar.css";
|
||||||
import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins";
|
import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins";
|
||||||
import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5";
|
import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5";
|
||||||
export { EditorWatchdog } from "ckeditor5";
|
export { EditorWatchdog } from "ckeditor5";
|
||||||
|
@ -23,6 +23,8 @@ import "@triliumnext/ckeditor5-mermaid/index.css";
|
|||||||
import "@triliumnext/ckeditor5-admonition/index.css";
|
import "@triliumnext/ckeditor5-admonition/index.css";
|
||||||
import "@triliumnext/ckeditor5-footnotes/index.css";
|
import "@triliumnext/ckeditor5-footnotes/index.css";
|
||||||
import "@triliumnext/ckeditor5-math/index.css";
|
import "@triliumnext/ckeditor5-math/index.css";
|
||||||
|
import CodeBlockToolbar from "./plugins/code_block_toolbar.js";
|
||||||
|
import CodeBlockLanguageDropdown from "./plugins/code_block_language_dropdown.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.
|
* 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.
|
||||||
@ -38,7 +40,9 @@ const TRILIUM_PLUGINS: typeof Plugin[] = [
|
|||||||
MarkdownImportPlugin,
|
MarkdownImportPlugin,
|
||||||
IncludeNote,
|
IncludeNote,
|
||||||
Uploadfileplugin,
|
Uploadfileplugin,
|
||||||
SyntaxHighlighting
|
SyntaxHighlighting,
|
||||||
|
CodeBlockLanguageDropdown,
|
||||||
|
CodeBlockToolbar
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
103
packages/ckeditor5/src/plugins/code_block_language_dropdown.ts
Normal file
103
packages/ckeditor5/src/plugins/code_block_language_dropdown.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { Editor, CodeBlock, Plugin, type ListDropdownButtonDefinition, Collection, type CodeBlockCommand, ViewModel, createDropdown, addListToDropdown, DropdownButtonView } from "ckeditor5";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toolbar item which displays the list of languages in a dropdown, with the text visible (similar to the headings switcher), as opposed to the default split button implementation.
|
||||||
|
*/
|
||||||
|
export default class CodeBlockLanguageDropdown extends Plugin {
|
||||||
|
|
||||||
|
static get requires() {
|
||||||
|
return [ CodeBlock ];
|
||||||
|
}
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
const editor = this.editor;
|
||||||
|
const componentFactory = editor.ui.componentFactory;
|
||||||
|
|
||||||
|
const normalizedLanguageDefs = this._getNormalizedAndLocalizedLanguageDefinitions(editor);
|
||||||
|
const itemDefinitions = this._getLanguageListItemDefinitions(normalizedLanguageDefs);
|
||||||
|
const command: CodeBlockCommand = editor.commands.get( 'codeBlock' )!;
|
||||||
|
|
||||||
|
componentFactory.add("codeBlockDropdown", locale => {
|
||||||
|
const dropdownView = createDropdown(this.editor.locale, DropdownButtonView);
|
||||||
|
dropdownView.buttonView.set({
|
||||||
|
withText: true
|
||||||
|
});
|
||||||
|
dropdownView.bind( 'isEnabled' ).to( command, 'value', value => !!value );
|
||||||
|
dropdownView.buttonView.bind( 'label' ).to( command, 'value', (value) => {
|
||||||
|
const itemDefinition = normalizedLanguageDefs.find((def) => def.language === value);
|
||||||
|
return itemDefinition?.label;
|
||||||
|
});
|
||||||
|
dropdownView.on( 'execute', evt => {
|
||||||
|
editor.execute( 'codeBlock', {
|
||||||
|
language: ( evt.source as any )._codeBlockLanguage,
|
||||||
|
forceValue: true
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.editing.view.focus();
|
||||||
|
});
|
||||||
|
addListToDropdown(dropdownView, itemDefinitions);
|
||||||
|
return dropdownView;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapted from packages/ckeditor5-code-block/src/codeblockui.ts
|
||||||
|
private _getLanguageListItemDefinitions(
|
||||||
|
normalizedLanguageDefs: Array<CodeBlockLanguageDefinition>
|
||||||
|
): Collection<ListDropdownButtonDefinition> {
|
||||||
|
const editor = this.editor;
|
||||||
|
const command: CodeBlockCommand = editor.commands.get( 'codeBlock' )!;
|
||||||
|
const itemDefinitions = new Collection<ListDropdownButtonDefinition>();
|
||||||
|
|
||||||
|
for ( const languageDef of normalizedLanguageDefs ) {
|
||||||
|
const definition: ListDropdownButtonDefinition = {
|
||||||
|
type: 'button',
|
||||||
|
model: new ViewModel( {
|
||||||
|
_codeBlockLanguage: languageDef.language,
|
||||||
|
label: languageDef.label,
|
||||||
|
role: 'menuitemradio',
|
||||||
|
withText: true
|
||||||
|
} )
|
||||||
|
};
|
||||||
|
|
||||||
|
definition.model.bind( 'isOn' ).to( command, 'value', value => {
|
||||||
|
return value === definition.model._codeBlockLanguage;
|
||||||
|
} );
|
||||||
|
|
||||||
|
itemDefinitions.add( definition );
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemDefinitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapted from packages/ckeditor5-code-block/src/utils.ts
|
||||||
|
private _getNormalizedAndLocalizedLanguageDefinitions(editor: Editor) {
|
||||||
|
const languageDefs = editor.config.get( 'codeBlock.languages' ) as Array<CodeBlockLanguageDefinition>;
|
||||||
|
for ( const def of languageDefs ) {
|
||||||
|
if ( def.class === undefined ) {
|
||||||
|
def.class = `language-${ def.language }`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return languageDefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodeBlockLanguageDefinition {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the language that will be stored in the model attribute. Also, when `class`
|
||||||
|
* is not specified, it will be used to create the CSS class associated with the language (prefixed by "language-").
|
||||||
|
*/
|
||||||
|
language: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The human–readable label associated with the language and displayed in the UI.
|
||||||
|
*/
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The CSS class associated with the language. When not specified the `language`
|
||||||
|
* property is used to create a class prefixed by "language-".
|
||||||
|
*/
|
||||||
|
class?: string;
|
||||||
|
}
|
42
packages/ckeditor5/src/plugins/code_block_toolbar.ts
Normal file
42
packages/ckeditor5/src/plugins/code_block_toolbar.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { CodeBlock, Plugin, ViewDocumentFragment, WidgetToolbarRepository, type ViewNode } from "ckeditor5";
|
||||||
|
import CodeBlockLanguageDropdown from "./code_block_language_dropdown";
|
||||||
|
import CopyToClipboardButton from "./copy_to_clipboard_button";
|
||||||
|
|
||||||
|
export default class CodeBlockToolbar extends Plugin {
|
||||||
|
|
||||||
|
static get requires() {
|
||||||
|
return [ WidgetToolbarRepository, CodeBlock, CodeBlockLanguageDropdown, CopyToClipboardButton ] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterInit() {
|
||||||
|
const editor = this.editor;
|
||||||
|
const widgetToolbarRepository = editor.plugins.get(WidgetToolbarRepository);
|
||||||
|
|
||||||
|
widgetToolbarRepository.register("codeblock", {
|
||||||
|
items: [
|
||||||
|
"codeBlockDropdown",
|
||||||
|
"|",
|
||||||
|
"copyToClipboard"
|
||||||
|
],
|
||||||
|
balloonClassName: "ck-toolbar-container codeblock-language-list",
|
||||||
|
getRelatedElement(selection) {
|
||||||
|
const selectionPosition = selection.getFirstPosition();
|
||||||
|
if (!selectionPosition) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent: ViewNode | ViewDocumentFragment | null = selectionPosition.parent;
|
||||||
|
while (parent) {
|
||||||
|
if (parent.is("element", "pre")) {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent = parent.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
56
packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts
Normal file
56
packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { ButtonView, Command, Plugin } from "ckeditor5";
|
||||||
|
import copyIcon from "../icons/copy.svg?raw";
|
||||||
|
|
||||||
|
export default class CopyToClipboardButton extends Plugin {
|
||||||
|
|
||||||
|
public init() {
|
||||||
|
const editor = this.editor;
|
||||||
|
editor.commands.add("copyToClipboard", new CopyToClipboardCommand(this.editor));
|
||||||
|
|
||||||
|
const componentFactory = editor.ui.componentFactory;
|
||||||
|
componentFactory.add("copyToClipboard", locale => {
|
||||||
|
const button = new ButtonView(locale);
|
||||||
|
button.set({
|
||||||
|
tooltip: "Copy to clipboard",
|
||||||
|
icon: copyIcon
|
||||||
|
});
|
||||||
|
|
||||||
|
this.listenTo(button, "execute", () => {
|
||||||
|
editor.execute("copyToClipboard");
|
||||||
|
});
|
||||||
|
|
||||||
|
return button;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CopyToClipboardCommand extends Command {
|
||||||
|
|
||||||
|
execute(...args: Array<unknown>) {
|
||||||
|
const editor = this.editor;
|
||||||
|
const model = editor.model;
|
||||||
|
const selection = model.document.selection;
|
||||||
|
|
||||||
|
const codeBlockEl = selection.getFirstPosition()?.findAncestor("codeBlock");
|
||||||
|
if (!codeBlockEl) {
|
||||||
|
console.warn("Unable to find code block element to copy from.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeText = Array.from(codeBlockEl.getChildren())
|
||||||
|
.map(child => "data" in child ? child.data : "\n")
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
if (codeText) {
|
||||||
|
navigator.clipboard.writeText(codeText).then(() => {
|
||||||
|
console.log('Code block copied to clipboard');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Failed to copy code block', err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('No code block selected or found.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
4
packages/ckeditor5/src/theme/code_block_toolbar.css
Normal file
4
packages/ckeditor5/src/theme/code_block_toolbar.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.ck.ck-balloon-panel.codeblock-language-list .ck-dropdown__panel {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { normalizeMimeTypeForCKEditor, type MimeType } from "@triliumnext/common
|
|||||||
import syntaxDefinitions from "./syntax_highlighting.js";
|
import syntaxDefinitions from "./syntax_highlighting.js";
|
||||||
import { type Theme } from "./themes.js";
|
import { type Theme } from "./themes.js";
|
||||||
import { type HighlightOptions } from "highlight.js";
|
import { type HighlightOptions } from "highlight.js";
|
||||||
|
export type { HighlightResult, AutoHighlightResult } from "highlight.js";
|
||||||
|
|
||||||
export { default as Themes, type Theme } from "./themes.js";
|
export { default as Themes, type Theme } from "./themes.js";
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user