import { Command, type Element, type RootElement, type Writer } from "ckeditor5"; import { ATTRIBUTES, ELEMENTS } from './constants.js'; import { modelQueryElement } from './utils.js'; export default class InsertFootnoteCommand extends Command { /** * Creates a footnote reference with the given index, and creates a matching * footnote if one doesn't already exist. Also creates the footnote section * if it doesn't exist. If `footnoteIndex` is 0 (or not provided), the added * footnote is given the next unused index--e.g. 7, if 6 footnotes exist so far. */ public override execute( { footnoteIndex }: { footnoteIndex?: number } = { footnoteIndex: 0 } ): void { this.editor.model.enqueueChange( modelWriter => { const doc = this.editor.model.document; const rootElement = doc.getRoot(); if ( !rootElement ) { return; } const footnoteSection = this._getFootnoteSection( modelWriter, rootElement ); let index: string | undefined = undefined; let id: string | undefined = undefined; if ( footnoteIndex === 0 ) { index = `${ footnoteSection.maxOffset + 1 }`; id = Math.random().toString( 36 ).slice( 2 ); } else { index = `${ footnoteIndex }`; const matchingFootnote = modelQueryElement( this.editor, footnoteSection, element => element.is( 'element', ELEMENTS.footnoteItem ) && element.getAttribute( ATTRIBUTES.footnoteIndex ) === index ); if ( matchingFootnote ) { id = matchingFootnote.getAttribute( ATTRIBUTES.footnoteId ) as string; } } if ( !id || !index ) { return; } modelWriter.setSelection( doc.selection.getLastPosition() ); const footnoteReference = modelWriter.createElement( ELEMENTS.footnoteReference, { [ ATTRIBUTES.footnoteId ]: id, [ ATTRIBUTES.footnoteIndex ]: index } ); this.editor.model.insertContent( footnoteReference ); modelWriter.setSelection( footnoteReference, 'after' ); // if referencing an existing footnote if ( footnoteIndex !== 0 ) { return; } const footnoteContent = modelWriter.createElement( ELEMENTS.footnoteContent ); const footnoteItem = modelWriter.createElement( ELEMENTS.footnoteItem, { [ ATTRIBUTES.footnoteId ]: id, [ ATTRIBUTES.footnoteIndex ]: index } ); const footnoteBackLink = modelWriter.createElement( ELEMENTS.footnoteBackLink, { [ ATTRIBUTES.footnoteId ]: id } ); const p = modelWriter.createElement( 'paragraph' ); modelWriter.append( p, footnoteContent ); modelWriter.append( footnoteContent, footnoteItem ); modelWriter.insert( footnoteBackLink, footnoteItem, 0 ); this.editor.model.insertContent( footnoteItem, modelWriter.createPositionAt( footnoteSection, footnoteSection.maxOffset ) ); } ); } /** * Called automatically when changes are applied to the document. Sets `isEnabled` * to determine whether footnote creation is allowed at the current location. */ public override refresh(): void { const model = this.editor.model; const lastPosition = model.document.selection.getLastPosition(); const allowedIn = lastPosition && model.schema.findAllowedParent( lastPosition, ELEMENTS.footnoteReference ); this.isEnabled = allowedIn !== null; } /** * Returns the footnote section if it exists, or creates on if it doesn't. */ private _getFootnoteSection( writer: Writer, rootElement: RootElement ): Element { const footnoteSection = modelQueryElement( this.editor, rootElement, element => element.is( 'element', ELEMENTS.footnoteSection ) ); if ( footnoteSection ) { return footnoteSection; } const newFootnoteSection = writer.createElement( ELEMENTS.footnoteSection ); this.editor.model.insertContent( newFootnoteSection, writer.createPositionAt( rootElement, rootElement.maxOffset ) ); return newFootnoteSection; } }