feat(admonitions): allow selecting admonition type

This commit is contained in:
Elian Doran
2025-03-13 21:27:29 +02:00
parent d29edbe325
commit fb7e310224
3 changed files with 53 additions and 20 deletions

View File

@@ -16,6 +16,10 @@ import type { DocumentFragment, Element, Position, Range, Schema, Writer } from
* *
* @extends module:core/command~Command * @extends module:core/command~Command
*/ */
// TODO: Change me.
type AdmonitionType = string;
export default class AdmonitionCommand extends Command { export default class AdmonitionCommand extends Command {
/** /**
* Whether the selection starts in a block quote. * Whether the selection starts in a block quote.
@@ -23,7 +27,7 @@ export default class AdmonitionCommand extends Command {
* @observable * @observable
* @readonly * @readonly
*/ */
declare public value: boolean; declare public value: AdmonitionType | false;
/** /**
* @inheritDoc * @inheritDoc
@@ -43,7 +47,7 @@ export default class AdmonitionCommand extends Command {
* @param options.forceValue If set, it will force the command behavior. If `true`, the command will apply a block quote, * @param options.forceValue If set, it will force the command behavior. If `true`, the command will apply a block quote,
* otherwise the command will remove the block quote. If not set, the command will act basing on its current value. * otherwise the command will remove the block quote. If not set, the command will act basing on its current value.
*/ */
public override execute( options: { forceValue?: boolean } = {} ): void { public override execute( options: { forceValue?: AdmonitionType } = {} ): void {
const model = this.editor.model; const model = this.editor.model;
const schema = model.schema; const schema = model.schema;
const selection = model.document.selection; const selection = model.document.selection;
@@ -51,6 +55,8 @@ export default class AdmonitionCommand extends Command {
const blocks = Array.from( selection.getSelectedBlocks() ); const blocks = Array.from( selection.getSelectedBlocks() );
const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue; const value = ( options.forceValue === undefined ) ? !this.value : options.forceValue;
// TODO: Fix me.
const valueString = (typeof value === "string" ? value : "note");
model.change( writer => { model.change( writer => {
if ( !value ) { if ( !value ) {
@@ -62,7 +68,7 @@ export default class AdmonitionCommand extends Command {
return findQuote( block ) || checkCanBeQuoted( schema, block ); return findQuote( block ) || checkCanBeQuoted( schema, block );
} ); } );
this._applyQuote( writer, blocksToQuote ); this._applyQuote( writer, blocksToQuote, valueString);
} }
} ); } );
} }
@@ -70,13 +76,15 @@ export default class AdmonitionCommand extends Command {
/** /**
* Checks the command's {@link #value}. * Checks the command's {@link #value}.
*/ */
private _getValue(): boolean { private _getValue(): AdmonitionType | false {
const selection = this.editor.model.document.selection; const selection = this.editor.model.document.selection;
const firstBlock = first( selection.getSelectedBlocks() ); const firstBlock = first( selection.getSelectedBlocks() );
// In the current implementation, the block quote must be an immediate parent of a block element. // In the current implementation, the block quote must be an immediate parent of a block element.
return !!( firstBlock && findQuote( firstBlock ) ); // TODO: Read correct quote.
const result = !!( firstBlock && findQuote( firstBlock ) );
return result ? "note" : false;
} }
/** /**
@@ -143,7 +151,7 @@ export default class AdmonitionCommand extends Command {
/** /**
* Applies the quote to given blocks. * Applies the quote to given blocks.
*/ */
private _applyQuote( writer: Writer, blocks: Array<Element> ): void { private _applyQuote( writer: Writer, blocks: Array<Element>, type?: AdmonitionType | false): void {
const quotesToMerge: Array<Element | DocumentFragment> = []; const quotesToMerge: Array<Element | DocumentFragment> = [];
// Quote all groups of block. Iterate in the reverse order to not break following ranges. // Quote all groups of block. Iterate in the reverse order to not break following ranges.
@@ -151,7 +159,7 @@ export default class AdmonitionCommand extends Command {
let quote = findQuote( groupRange.start ); let quote = findQuote( groupRange.start );
if ( !quote ) { if ( !quote ) {
quote = writer.createElement( 'aside' ); quote = writer.createElement( 'aside', { type });
writer.wrap( groupRange, quote ); writer.wrap( groupRange, quote );
} }

View File

@@ -12,6 +12,7 @@ import { Enter, type ViewDocumentEnterEvent } from 'ckeditor5/src/enter.js';
import { Delete, type ViewDocumentDeleteEvent } from 'ckeditor5/src/typing.js'; import { Delete, type ViewDocumentDeleteEvent } from 'ckeditor5/src/typing.js';
import AdmonitionCommand from './admonitioncommand.js'; import AdmonitionCommand from './admonitioncommand.js';
import { ADMONITION_TYPES } from './admonitionui.js';
/** /**
* The block quote editing. * The block quote editing.
@@ -45,14 +46,38 @@ export default class AdmonitionEditing extends Plugin {
editor.commands.add( 'admonition', new AdmonitionCommand( editor ) ); editor.commands.add( 'admonition', new AdmonitionCommand( editor ) );
schema.register( 'aside', { schema.register( 'aside', {
inheritAllFrom: '$container' inheritAllFrom: '$container',
allowAttributes: "type"
} ); } );
editor.conversion.elementToElement( { editor.conversion.for("upcast").elementToElement({
model: 'aside',
view: { view: {
name: "aside", name: "aside",
classes: "admonition" classes: "admonition",
},
model: (viewElement, { writer }) => {
let type = "note";
const allowedTypes = Object.keys(ADMONITION_TYPES);
for (const className of viewElement.getClassNames()) {
if (className !== "admonition" && allowedTypes.includes(className)) {
type = className;
}
}
return writer.createElement("aside", {
type
});
}
});
editor.conversion.for("downcast").elementToElement( {
model: 'aside',
view: (modelElement, { writer }) => {
return writer.createContainerElement(
"aside", {
class: [ "admonition", modelElement.getAttribute("type") ].join(" ")
}
)
} }
}); });

View File

@@ -18,7 +18,7 @@ interface AdmonitionDefinition {
title: string; title: string;
} }
const ADMONITION_TYPES: Record<string, AdmonitionDefinition> = { export const ADMONITION_TYPES: Record<string, AdmonitionDefinition> = {
"note": { "note": {
title: "Note" title: "Note"
}, },
@@ -71,28 +71,28 @@ export default class AdmonitionUI extends Plugin {
const editor = this.editor; const editor = this.editor;
const locale = editor.locale; const locale = editor.locale;
const command = editor.commands.get( 'admonition' )!; const command = editor.commands.get( 'admonition' )!;
const view = createDropdown(locale); const dropdownView = createDropdown(locale);
const t = locale.t; const t = locale.t;
addListToDropdown(view, this._getDropdownItems()) addListToDropdown(dropdownView, this._getDropdownItems())
view.buttonView.set( { dropdownView.buttonView.set( {
label: t( 'Admonition' ), label: t( 'Admonition' ),
icon: admonitionIcon, icon: admonitionIcon,
isToggleable: true, isToggleable: true,
tooltip: true tooltip: true
} ); } );
view.bind( 'isEnabled' ).to( command, 'isEnabled' ); dropdownView.bind( 'isEnabled' ).to( command, 'isEnabled' );
// view.buttonView.bind( 'isOn' ).to( command, 'value' ); // view.buttonView.bind( 'isOn' ).to( command, 'value' );
// Execute the command. // Execute the command.
this.listenTo( view, 'execute', () => { this.listenTo(dropdownView, 'execute', evt => {
editor.execute( 'admonition' ); editor.execute("admonition", { forceValue: ( evt.source as any ).commandParam } );
editor.editing.view.focus(); editor.editing.view.focus();
} ); });
return view; return dropdownView;
} }
private _getDropdownItems() { private _getDropdownItems() {