From a1d5719fe06d41b3c8b480b0bcbd94c22e4c528d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 09:17:35 +0300 Subject: [PATCH 01/14] feat(ckeditor5): create an empty toolbar for code blocks --- packages/ckeditor5/src/plugins.ts | 4 +- .../src/plugins/code_block_toolbar.ts | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 packages/ckeditor5/src/plugins/code_block_toolbar.ts diff --git a/packages/ckeditor5/src/plugins.ts b/packages/ckeditor5/src/plugins.ts index 19fbc748e..c53dfb924 100644 --- a/packages/ckeditor5/src/plugins.ts +++ b/packages/ckeditor5/src/plugins.ts @@ -23,6 +23,7 @@ import "@triliumnext/ckeditor5-mermaid/index.css"; import "@triliumnext/ckeditor5-admonition/index.css"; import "@triliumnext/ckeditor5-footnotes/index.css"; import "@triliumnext/ckeditor5-math/index.css"; +import CodeBlockToolbar from "./plugins/code_block_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. @@ -38,7 +39,8 @@ const TRILIUM_PLUGINS: typeof Plugin[] = [ MarkdownImportPlugin, IncludeNote, Uploadfileplugin, - SyntaxHighlighting + SyntaxHighlighting, + CodeBlockToolbar ]; /** diff --git a/packages/ckeditor5/src/plugins/code_block_toolbar.ts b/packages/ckeditor5/src/plugins/code_block_toolbar.ts new file mode 100644 index 000000000..58e322381 --- /dev/null +++ b/packages/ckeditor5/src/plugins/code_block_toolbar.ts @@ -0,0 +1,45 @@ +import { CodeBlock, Plugin, Position, ViewDocumentFragment, WidgetToolbarRepository, type Node, type ViewNode } from "ckeditor5"; + +export default class CodeBlockToolbar extends Plugin { + + static get requires() { + return [ WidgetToolbarRepository, CodeBlock ] as const; + } + + afterInit() { + const editor = this.editor; + const widgetToolbarRepository = editor.plugins.get(WidgetToolbarRepository); + + widgetToolbarRepository.register("codeblock", { + items: [ + { + label: "Hello", + items: [ + { + label: "world", + items: [] + } + ] + } + ], + 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; + } + }); + } + +} From 178ce310643ee122991bf8dcfb66a27a3fd1f86a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 10:07:52 +0300 Subject: [PATCH 02/14] feat(ckeditor5/codeblock): add language dropdown --- .../src/plugins/code_block_toolbar.ts | 103 ++++++++++++++++-- 1 file changed, 93 insertions(+), 10 deletions(-) diff --git a/packages/ckeditor5/src/plugins/code_block_toolbar.ts b/packages/ckeditor5/src/plugins/code_block_toolbar.ts index 58e322381..259f98bc4 100644 --- a/packages/ckeditor5/src/plugins/code_block_toolbar.ts +++ b/packages/ckeditor5/src/plugins/code_block_toolbar.ts @@ -1,4 +1,4 @@ -import { CodeBlock, Plugin, Position, ViewDocumentFragment, WidgetToolbarRepository, type Node, type ViewNode } from "ckeditor5"; +import { Editor, CodeBlock, Plugin, ViewDocumentFragment, WidgetToolbarRepository, type ViewNode, type ListDropdownButtonDefinition, Collection, type CodeBlockCommand, ViewModel, createDropdown, addListToDropdown, DropdownButtonView } from "ckeditor5"; export default class CodeBlockToolbar extends Plugin { @@ -6,21 +6,44 @@ export default class CodeBlockToolbar extends Plugin { return [ WidgetToolbarRepository, CodeBlock ] as const; } + public init(): void { + 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; + }); + } + afterInit() { const editor = this.editor; const widgetToolbarRepository = editor.plugins.get(WidgetToolbarRepository); widgetToolbarRepository.register("codeblock", { items: [ - { - label: "Hello", - items: [ - { - label: "world", - items: [] - } - ] - } + "codeBlockDropdown" ], getRelatedElement(selection) { const selectionPosition = selection.getFirstPosition(); @@ -42,4 +65,64 @@ export default class CodeBlockToolbar extends Plugin { }); } + // Adapted from packages/ckeditor5-code-block/src/codeblockui.ts + private _getLanguageListItemDefinitions( + normalizedLanguageDefs: Array + ): Collection { + const editor = this.editor; + const command: CodeBlockCommand = editor.commands.get( 'codeBlock' )!; + const itemDefinitions = new Collection(); + + 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; + 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; } From 751ed0b5d4706faeae53efb9c647f7571e3b3e2b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 10:53:12 +0300 Subject: [PATCH 03/14] refactor(ckeditor5/codeblock): split dropdown into own plugin --- packages/ckeditor5/src/plugins.ts | 2 + .../plugins/code_block_language_dropdown.ts | 103 ++++++++++++++++++ .../src/plugins/code_block_toolbar.ts | 96 +--------------- 3 files changed, 108 insertions(+), 93 deletions(-) create mode 100644 packages/ckeditor5/src/plugins/code_block_language_dropdown.ts diff --git a/packages/ckeditor5/src/plugins.ts b/packages/ckeditor5/src/plugins.ts index c53dfb924..29abde14c 100644 --- a/packages/ckeditor5/src/plugins.ts +++ b/packages/ckeditor5/src/plugins.ts @@ -24,6 +24,7 @@ import "@triliumnext/ckeditor5-admonition/index.css"; import "@triliumnext/ckeditor5-footnotes/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. @@ -40,6 +41,7 @@ const TRILIUM_PLUGINS: typeof Plugin[] = [ IncludeNote, Uploadfileplugin, SyntaxHighlighting, + CodeBlockLanguageDropdown, CodeBlockToolbar ]; diff --git a/packages/ckeditor5/src/plugins/code_block_language_dropdown.ts b/packages/ckeditor5/src/plugins/code_block_language_dropdown.ts new file mode 100644 index 000000000..7b384a784 --- /dev/null +++ b/packages/ckeditor5/src/plugins/code_block_language_dropdown.ts @@ -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 + ): Collection { + const editor = this.editor; + const command: CodeBlockCommand = editor.commands.get( 'codeBlock' )!; + const itemDefinitions = new Collection(); + + 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; + 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; +} diff --git a/packages/ckeditor5/src/plugins/code_block_toolbar.ts b/packages/ckeditor5/src/plugins/code_block_toolbar.ts index 259f98bc4..8e4e1081a 100644 --- a/packages/ckeditor5/src/plugins/code_block_toolbar.ts +++ b/packages/ckeditor5/src/plugins/code_block_toolbar.ts @@ -1,40 +1,10 @@ -import { Editor, CodeBlock, Plugin, ViewDocumentFragment, WidgetToolbarRepository, type ViewNode, type ListDropdownButtonDefinition, Collection, type CodeBlockCommand, ViewModel, createDropdown, addListToDropdown, DropdownButtonView } from "ckeditor5"; +import { CodeBlock, Plugin, ViewDocumentFragment, WidgetToolbarRepository, type ViewNode } from "ckeditor5"; +import CodeBlockLanguageDropdown from "./code_block_language_dropdown"; export default class CodeBlockToolbar extends Plugin { static get requires() { - return [ WidgetToolbarRepository, CodeBlock ] as const; - } - - public init(): void { - 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; - }); + return [ WidgetToolbarRepository, CodeBlock, CodeBlockLanguageDropdown ] as const; } afterInit() { @@ -65,64 +35,4 @@ export default class CodeBlockToolbar extends Plugin { }); } - // Adapted from packages/ckeditor5-code-block/src/codeblockui.ts - private _getLanguageListItemDefinitions( - normalizedLanguageDefs: Array - ): Collection { - const editor = this.editor; - const command: CodeBlockCommand = editor.commands.get( 'codeBlock' )!; - const itemDefinitions = new Collection(); - - 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; - 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; } From 5eecea52bf42c1434fe0a739c2f1f05cbef591c0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 11:37:26 +0300 Subject: [PATCH 04/14] feat(ckeditor5/codeblock): add copy icon --- packages/ckeditor5/src/icons/copy.svg | 1 + .../src/plugins/code_block_toolbar.ts | 7 +++++-- .../src/plugins/copy_to_clipboard_button.ts | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 packages/ckeditor5/src/icons/copy.svg create mode 100644 packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts diff --git a/packages/ckeditor5/src/icons/copy.svg b/packages/ckeditor5/src/icons/copy.svg new file mode 100644 index 000000000..41710638b --- /dev/null +++ b/packages/ckeditor5/src/icons/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/ckeditor5/src/plugins/code_block_toolbar.ts b/packages/ckeditor5/src/plugins/code_block_toolbar.ts index 8e4e1081a..4f886efa8 100644 --- a/packages/ckeditor5/src/plugins/code_block_toolbar.ts +++ b/packages/ckeditor5/src/plugins/code_block_toolbar.ts @@ -1,10 +1,11 @@ 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 ] as const; + return [ WidgetToolbarRepository, CodeBlock, CodeBlockLanguageDropdown, CopyToClipboardButton ] as const; } afterInit() { @@ -13,7 +14,9 @@ export default class CodeBlockToolbar extends Plugin { widgetToolbarRepository.register("codeblock", { items: [ - "codeBlockDropdown" + "codeBlockDropdown", + "|", + "copyToClipboard" ], getRelatedElement(selection) { const selectionPosition = selection.getFirstPosition(); diff --git a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts new file mode 100644 index 000000000..2b67ea820 --- /dev/null +++ b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts @@ -0,0 +1,21 @@ +import { ButtonView, Plugin } from "ckeditor5"; +import copyIcon from "../icons/copy.svg?raw"; + +export default class CopyToClipboardButton extends Plugin { + + public init() { + const editor = this.editor; + const componentFactory = editor.ui.componentFactory; + + componentFactory.add("copyToClipboard", locale => { + const button = new ButtonView(locale); + button.set({ + tooltip: "Copy to clipboard", + icon: copyIcon + }); + + return button; + }); + } + +} From fc83f67d7cc090b479035a225f82387263d3381b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 11:37:44 +0300 Subject: [PATCH 05/14] chore(ckeditor5/codeblock): add command for copying to clipboard --- .../src/plugins/copy_to_clipboard_button.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts index 2b67ea820..1de7e6f0e 100644 --- a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts +++ b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts @@ -1,8 +1,16 @@ -import { ButtonView, Plugin } from "ckeditor5"; +import { ButtonView, Command, Plugin } from "ckeditor5"; import copyIcon from "../icons/copy.svg?raw"; export default class CopyToClipboardButton extends Plugin { + static get requires() { + return [ CopyToClipboardEditing, CopyToClipboardUI ]; + } + +} + +export class CopyToClipboardUI extends Plugin { + public init() { const editor = this.editor; const componentFactory = editor.ui.componentFactory; @@ -14,8 +22,28 @@ export default class CopyToClipboardButton extends Plugin { icon: copyIcon }); + this.listenTo(button, "execute", () => { + editor.execute("copyToClipboard"); + }); + return button; }); } } + +export class CopyToClipboardEditing extends Plugin { + + public init() { + this.editor.commands.add("copyToClipboard", new CopyToClipboardCommand(this.editor)); + } + +} + +export class CopyToClipboardCommand extends Command { + + execute(...args: Array) { + console.log("Copy to clipboard!"); + } + +} From a77d89f4c7259a0b2049e649870c46703fd99776 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 12:18:21 +0300 Subject: [PATCH 06/14] feat(ckeditor5/codeblock): implement copy to clipboard function --- .../src/plugins/copy_to_clipboard_button.ts | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts index 1de7e6f0e..d212c1e49 100644 --- a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts +++ b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts @@ -43,7 +43,29 @@ export class CopyToClipboardEditing extends Plugin { export class CopyToClipboardCommand extends Command { execute(...args: Array) { - console.log("Copy to clipboard!"); + 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.'); + } } } From 622d026efc9f3e5e7f5ef5b23b2c8bc9f56bfc94 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 12:23:11 +0300 Subject: [PATCH 07/14] refactor(ckeditor5/codeblock): simplify copy clipboard plugin --- .../src/plugins/copy_to_clipboard_button.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts index d212c1e49..281259c17 100644 --- a/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts +++ b/packages/ckeditor5/src/plugins/copy_to_clipboard_button.ts @@ -3,18 +3,11 @@ import copyIcon from "../icons/copy.svg?raw"; export default class CopyToClipboardButton extends Plugin { - static get requires() { - return [ CopyToClipboardEditing, CopyToClipboardUI ]; - } - -} - -export class CopyToClipboardUI extends Plugin { - public init() { const editor = this.editor; - const componentFactory = editor.ui.componentFactory; + editor.commands.add("copyToClipboard", new CopyToClipboardCommand(this.editor)); + const componentFactory = editor.ui.componentFactory; componentFactory.add("copyToClipboard", locale => { const button = new ButtonView(locale); button.set({ @@ -32,14 +25,6 @@ export class CopyToClipboardUI extends Plugin { } -export class CopyToClipboardEditing extends Plugin { - - public init() { - this.editor.commands.add("copyToClipboard", new CopyToClipboardCommand(this.editor)); - } - -} - export class CopyToClipboardCommand extends Command { execute(...args: Array) { From 4752db6bc5ad4ca1c188a281cf8a838fa7bf2c63 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 12:35:30 +0300 Subject: [PATCH 08/14] style(ckeditor5/codeblock): limit language selector height --- packages/ckeditor5/src/index.ts | 1 + packages/ckeditor5/src/plugins/code_block_toolbar.ts | 1 + packages/ckeditor5/src/theme/code_block_toolbar.css | 4 ++++ 3 files changed, 6 insertions(+) create mode 100644 packages/ckeditor5/src/theme/code_block_toolbar.css diff --git a/packages/ckeditor5/src/index.ts b/packages/ckeditor5/src/index.ts index 1a614f8e8..8dc0e3611 100644 --- a/packages/ckeditor5/src/index.ts +++ b/packages/ckeditor5/src/index.ts @@ -1,4 +1,5 @@ import "ckeditor5/ckeditor5.css"; +import "./theme/code_block_toolbar.css"; import { COMMON_PLUGINS, CORE_PLUGINS, POPUP_EDITOR_PLUGINS } from "./plugins"; import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } from "ckeditor5"; export { EditorWatchdog } from "ckeditor5"; diff --git a/packages/ckeditor5/src/plugins/code_block_toolbar.ts b/packages/ckeditor5/src/plugins/code_block_toolbar.ts index 4f886efa8..ff9014fd8 100644 --- a/packages/ckeditor5/src/plugins/code_block_toolbar.ts +++ b/packages/ckeditor5/src/plugins/code_block_toolbar.ts @@ -18,6 +18,7 @@ export default class CodeBlockToolbar extends Plugin { "|", "copyToClipboard" ], + balloonClassName: "ck-toolbar-container codeblock-language-list", getRelatedElement(selection) { const selectionPosition = selection.getFirstPosition(); if (!selectionPosition) { diff --git a/packages/ckeditor5/src/theme/code_block_toolbar.css b/packages/ckeditor5/src/theme/code_block_toolbar.css new file mode 100644 index 000000000..0776571b4 --- /dev/null +++ b/packages/ckeditor5/src/theme/code_block_toolbar.css @@ -0,0 +1,4 @@ +.ck.ck-balloon-panel.codeblock-language-list .ck-dropdown__panel { + max-height: 300px; + overflow-y: auto; +} \ No newline at end of file From 02e2b5d4ad4a0bfc0e7c7f1ad3c3fcd1b03c2d53 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 26 May 2025 15:17:10 +0300 Subject: [PATCH 09/14] feat(client): add a copy button to read-only text --- apps/client/src/services/content_renderer.ts | 4 ++-- apps/client/src/services/doc_renderer.ts | 4 ++-- apps/client/src/services/syntax_highlight.ts | 22 ++++++++++++++----- apps/client/src/stylesheets/style.css | 20 +++++++++++++++++ .../src/widgets/llm_chat/llm_chat_panel.ts | 4 ++-- apps/client/src/widgets/llm_chat/utils.ts | 4 ++-- .../widgets/type_widgets/read_only_text.ts | 4 ++-- 7 files changed, 46 insertions(+), 16 deletions(-) diff --git a/apps/client/src/services/content_renderer.ts b/apps/client/src/services/content_renderer.ts index 0664f6a5c..08ed561ff 100644 --- a/apps/client/src/services/content_renderer.ts +++ b/apps/client/src/services/content_renderer.ts @@ -9,7 +9,7 @@ import treeService from "./tree.js"; import FNote from "../entities/fnote.js"; import FAttachment from "../entities/fattachment.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 renderDoc from "./doc_renderer.js"; import { t } from "../services/i18n.js"; @@ -106,7 +106,7 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery>((resolve) => { @@ -41,7 +41,7 @@ function processContent(url: string, $content: JQuery) { $img.attr("src", dir + "/" + $img.attr("src")); }); - applySyntaxHighlight($content); + formatCodeBlocks($content); } function getUrl(docNameValue: string, language: string) { diff --git a/apps/client/src/services/syntax_highlight.ts b/apps/client/src/services/syntax_highlight.ts index 7dfb29f30..0cb7cbf2d 100644 --- a/apps/client/src/services/syntax_highlight.ts +++ b/apps/client/src/services/syntax_highlight.ts @@ -6,16 +6,16 @@ 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. + * 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. */ -export async function applySyntaxHighlight($container: JQuery) { - if (!isSyntaxHighlightEnabled()) { - return; +export async function formatCodeBlocks($container: JQuery) { + const syntaxHighlightingEnabled = isSyntaxHighlightEnabled(); + if (syntaxHighlightingEnabled) { + await ensureMimeTypesForHighlighting(); } - await ensureMimeTypesForHighlighting(); - const codeBlocks = $container.find("pre code"); for (const codeBlock of codeBlocks) { const normalizedMimeType = extractLanguageFromClassList(codeBlock); @@ -23,10 +23,20 @@ export async function applySyntaxHighlight($container: JQuery) { continue; } - applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType); + applyCopyToClipboardButton($(codeBlock)); + + if (syntaxHighlightingEnabled) { + applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType); + } } } +export function applyCopyToClipboardButton($codeBlock: JQuery) { + const $copyButton = $("