mirror of
https://github.com/zadam/trilium.git
synced 2026-05-06 19:06:35 +02:00
feat(ckeditor/include_note): add a way to change size after creation (closes #3705)
This commit is contained in:
@@ -34,6 +34,8 @@ import InlineCodeNoSpellcheck from "./plugins/inline_code_no_spellcheck.js";
|
||||
import InlineCodeToolbar from "./plugins/inline_code_toolbar.js";
|
||||
import AdmonitionTypeDropdown from "./plugins/admonition_type_dropdown.js";
|
||||
import AdmonitionToolbar from "./plugins/admonition_toolbar.js";
|
||||
import IncludeNoteBoxSizeDropdown from "./plugins/include_note_box_size_dropdown.js";
|
||||
import IncludeNoteToolbar from "./plugins/include_note_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.
|
||||
@@ -59,6 +61,8 @@ const TRILIUM_PLUGINS: typeof Plugin[] = [
|
||||
InlineCodeToolbar,
|
||||
AdmonitionTypeDropdown,
|
||||
AdmonitionToolbar,
|
||||
IncludeNoteBoxSizeDropdown,
|
||||
IncludeNoteToolbar,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Plugin, type ListDropdownButtonDefinition, Collection, ViewModel, createDropdown, addListToDropdown, DropdownButtonView, type Command } from "ckeditor5";
|
||||
import IncludeNote, { BOX_SIZE_COMMAND_NAME, BOX_SIZES, type BoxSizeValue } from "./includenote.js";
|
||||
|
||||
/**
|
||||
* Toolbar item which displays the list of box sizes for include notes in a dropdown.
|
||||
*/
|
||||
export default class IncludeNoteBoxSizeDropdown extends Plugin {
|
||||
|
||||
static get requires() {
|
||||
return [IncludeNote] as const;
|
||||
}
|
||||
|
||||
public init() {
|
||||
const editor = this.editor;
|
||||
const componentFactory = editor.ui.componentFactory;
|
||||
|
||||
console.log("[IncludeNoteBoxSizeDropdown] Initializing");
|
||||
|
||||
const itemDefinitions = this._getBoxSizeListItemDefinitions();
|
||||
const command = editor.commands.get(BOX_SIZE_COMMAND_NAME) as Command & { value: BoxSizeValue | null };
|
||||
|
||||
console.log("[IncludeNoteBoxSizeDropdown] Command:", command);
|
||||
|
||||
componentFactory.add("includeNoteBoxSizeDropdown", _locale => {
|
||||
console.log("[IncludeNoteBoxSizeDropdown] Creating dropdown component");
|
||||
const dropdownView = createDropdown(editor.locale, DropdownButtonView);
|
||||
dropdownView.buttonView.set({
|
||||
withText: true,
|
||||
tooltip: true,
|
||||
label: "Box size"
|
||||
});
|
||||
dropdownView.bind("isEnabled").to(command, "isEnabled");
|
||||
dropdownView.buttonView.bind("label").to(command, "value", (value) => {
|
||||
if (!value) return "Box size";
|
||||
const sizeDef = BOX_SIZES.find(s => s.value === value);
|
||||
return sizeDef?.label ?? value;
|
||||
});
|
||||
dropdownView.on("execute", evt => {
|
||||
const source = evt.source as any;
|
||||
editor.execute(BOX_SIZE_COMMAND_NAME, {
|
||||
value: source._boxSizeValue
|
||||
});
|
||||
editor.editing.view.focus();
|
||||
});
|
||||
addListToDropdown(dropdownView, itemDefinitions);
|
||||
return dropdownView;
|
||||
});
|
||||
}
|
||||
|
||||
private _getBoxSizeListItemDefinitions(): Collection<ListDropdownButtonDefinition> {
|
||||
const editor = this.editor;
|
||||
const command = editor.commands.get(BOX_SIZE_COMMAND_NAME) as Command & { value: BoxSizeValue | null };
|
||||
const itemDefinitions = new Collection<ListDropdownButtonDefinition>();
|
||||
|
||||
for (const sizeDef of BOX_SIZES) {
|
||||
const definition: ListDropdownButtonDefinition = {
|
||||
type: "button",
|
||||
model: new ViewModel({
|
||||
_boxSizeValue: sizeDef.value,
|
||||
label: sizeDef.label,
|
||||
role: "menuitemradio",
|
||||
withText: true
|
||||
})
|
||||
};
|
||||
|
||||
definition.model.bind("isOn").to(command, "value", value => {
|
||||
return value === sizeDef.value;
|
||||
});
|
||||
|
||||
itemDefinitions.add(definition);
|
||||
}
|
||||
|
||||
return itemDefinitions;
|
||||
}
|
||||
|
||||
}
|
||||
56
packages/ckeditor5/src/plugins/include_note_toolbar.ts
Normal file
56
packages/ckeditor5/src/plugins/include_note_toolbar.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Plugin, WidgetToolbarRepository, isWidget, type ViewElement } from "ckeditor5";
|
||||
import IncludeNote from "./includenote.js";
|
||||
import IncludeNoteBoxSizeDropdown from "./include_note_box_size_dropdown.js";
|
||||
|
||||
export default class IncludeNoteToolbar extends Plugin {
|
||||
|
||||
static get requires() {
|
||||
return [WidgetToolbarRepository, IncludeNote, IncludeNoteBoxSizeDropdown] as const;
|
||||
}
|
||||
|
||||
afterInit() {
|
||||
const editor = this.editor;
|
||||
const widgetToolbarRepository = editor.plugins.get(WidgetToolbarRepository);
|
||||
|
||||
console.log("[IncludeNoteToolbar] Registering toolbar");
|
||||
|
||||
widgetToolbarRepository.register("includeNote", {
|
||||
items: [
|
||||
"includeNoteBoxSizeDropdown"
|
||||
],
|
||||
balloonClassName: "ck-toolbar-container include-note-toolbar",
|
||||
getRelatedElement(selection) {
|
||||
const selectedElement = selection.getSelectedElement();
|
||||
console.log("[IncludeNoteToolbar] getRelatedElement called, selectedElement:", selectedElement);
|
||||
|
||||
if (selectedElement) {
|
||||
console.log("[IncludeNoteToolbar] Element name:", selectedElement.name);
|
||||
console.log("[IncludeNoteToolbar] Element classes:", selectedElement.getAttribute("class"));
|
||||
console.log("[IncludeNoteToolbar] isWidget:", isWidget(selectedElement));
|
||||
}
|
||||
|
||||
if (selectedElement && isIncludeNoteWidget(selectedElement)) {
|
||||
console.log("[IncludeNoteToolbar] Found include note widget, returning element");
|
||||
return selectedElement;
|
||||
}
|
||||
|
||||
console.log("[IncludeNoteToolbar] No include note widget found");
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function isIncludeNoteWidget(element: ViewElement): boolean {
|
||||
if (!isWidget(element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!element.is("element", "section")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const classes = element.getAttribute("class") || "";
|
||||
return typeof classes === "string" && classes.includes("include-note");
|
||||
}
|
||||
@@ -2,6 +2,15 @@ import { ButtonView, Command, Plugin, toWidget, Widget, type Editor, type Observ
|
||||
import noteIcon from '../icons/note.svg?raw';
|
||||
|
||||
export const COMMAND_NAME = 'insertIncludeNote';
|
||||
export const BOX_SIZE_COMMAND_NAME = 'includeNoteBoxSize';
|
||||
|
||||
export const BOX_SIZES = [
|
||||
{ value: 'small', label: 'Small' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'full', label: 'Full' }
|
||||
] as const;
|
||||
|
||||
export type BoxSizeValue = typeof BOX_SIZES[number]['value'];
|
||||
|
||||
export default class IncludeNote extends Plugin {
|
||||
static get requires() {
|
||||
@@ -54,6 +63,7 @@ class IncludeNoteEditing extends Plugin {
|
||||
this._defineConverters();
|
||||
|
||||
this.editor.commands.add( COMMAND_NAME, new InsertIncludeNoteCommand( this.editor ) );
|
||||
this.editor.commands.add( BOX_SIZE_COMMAND_NAME, new IncludeNoteBoxSizeCommand( this.editor ) );
|
||||
}
|
||||
|
||||
_defineSchema() {
|
||||
@@ -133,6 +143,29 @@ class IncludeNoteEditing extends Plugin {
|
||||
return toWidget( section, viewWriter, { label: 'include note widget' } );
|
||||
}
|
||||
} );
|
||||
|
||||
// Handle boxSize attribute changes on existing elements
|
||||
conversion.for( 'editingDowncast' ).add( dispatcher => {
|
||||
dispatcher.on( 'attribute:boxSize:includeNote', ( evt, data, conversionApi ) => {
|
||||
const viewElement = conversionApi.mapper.toViewElement( data.item );
|
||||
if ( !viewElement ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewWriter = conversionApi.writer;
|
||||
const oldBoxSize = data.attributeOldValue as string;
|
||||
const newBoxSize = data.attributeNewValue as string;
|
||||
|
||||
// Remove old class and add new class
|
||||
if ( oldBoxSize ) {
|
||||
viewWriter.removeClass( 'box-size-' + oldBoxSize, viewElement );
|
||||
}
|
||||
if ( newBoxSize ) {
|
||||
viewWriter.addClass( 'box-size-' + newBoxSize, viewElement );
|
||||
viewWriter.setAttribute( 'data-box-size', newBoxSize, viewElement );
|
||||
}
|
||||
} );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +187,43 @@ class InsertIncludeNoteCommand extends Command {
|
||||
}
|
||||
}
|
||||
|
||||
class IncludeNoteBoxSizeCommand extends Command {
|
||||
declare value: BoxSizeValue | null;
|
||||
|
||||
override execute( options: { value: BoxSizeValue } ) {
|
||||
console.log("[IncludeNoteBoxSizeCommand] execute called with:", options);
|
||||
const model = this.editor.model;
|
||||
const includeNoteElement = this._getSelectedIncludeNote();
|
||||
|
||||
if ( includeNoteElement ) {
|
||||
model.change( writer => {
|
||||
writer.setAttribute( 'boxSize', options.value, includeNoteElement );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
override refresh() {
|
||||
const includeNoteElement = this._getSelectedIncludeNote();
|
||||
|
||||
this.isEnabled = !!includeNoteElement;
|
||||
this.value = includeNoteElement?.getAttribute( 'boxSize' ) as BoxSizeValue | null ?? null;
|
||||
console.log("[IncludeNoteBoxSizeCommand] refresh - isEnabled:", this.isEnabled, "value:", this.value);
|
||||
}
|
||||
|
||||
private _getSelectedIncludeNote() {
|
||||
const selection = this.editor.model.document.selection;
|
||||
const selectedElement = selection.getSelectedElement();
|
||||
|
||||
if ( selectedElement?.name === 'includeNote' ) {
|
||||
return selectedElement;
|
||||
}
|
||||
|
||||
// Check if we're inside an include note
|
||||
const firstPosition = selection.getFirstPosition();
|
||||
return firstPosition?.findAncestor( 'includeNote' ) ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hack coming from https://github.com/ckeditor/ckeditor5/issues/4465
|
||||
* Source issue: https://github.com/zadam/trilium/issues/1117
|
||||
@@ -163,7 +233,15 @@ function preventCKEditorHandling( domElement: HTMLElement, editor: Editor ) {
|
||||
|
||||
// commenting out click events to allow link click handler to still work
|
||||
//domElement.addEventListener( 'click', stopEventPropagationAndHackRendererFocus, { capture: true } );
|
||||
domElement.addEventListener( 'mousedown', stopEventPropagationAndHackRendererFocus, { capture: true } );
|
||||
domElement.addEventListener( 'mousedown', ( evt: Event ) => {
|
||||
evt.stopPropagation();
|
||||
// This prevents rendering changed view selection thus preventing to changing DOM selection while inside a widget.
|
||||
//@ts-expect-error: We are accessing a private field.
|
||||
editor.editing.view._renderer.isFocused = false;
|
||||
|
||||
// Select the widget when clicking inside it
|
||||
selectIncludeNoteWidget( domElement, editor );
|
||||
}, { capture: true } );
|
||||
domElement.addEventListener( 'focus', stopEventPropagationAndHackRendererFocus, { capture: true } );
|
||||
|
||||
// Prevents TAB handling or other editor keys listeners which might be executed on editors selection.
|
||||
@@ -176,3 +254,28 @@ function preventCKEditorHandling( domElement: HTMLElement, editor: Editor ) {
|
||||
editor.editing.view._renderer.isFocused = false;
|
||||
}
|
||||
}
|
||||
|
||||
function selectIncludeNoteWidget( domElement: HTMLElement, editor: Editor ) {
|
||||
// Find the parent section element (the widget container)
|
||||
const sectionElement = domElement.closest( 'section.include-note' ) as HTMLElement | null;
|
||||
if ( !sectionElement ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the view element from the DOM element
|
||||
const viewElement = editor.editing.view.domConverter.mapDomToView( sectionElement );
|
||||
if ( !viewElement || !viewElement.is( 'element' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the model element from the view element
|
||||
const modelElement = editor.editing.mapper.toModelElement( viewElement );
|
||||
if ( !modelElement ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select the model element
|
||||
editor.model.change( writer => {
|
||||
writer.setSelection( modelElement, 'on' );
|
||||
} );
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user