import { Plugin } from 'ckeditor5/src/core'; import { ButtonView } from 'ckeditor5/src/ui'; import { Widget, toWidget } from 'ckeditor5/src/widget'; import icon from './reference-footnotes-icon.svg'; /** * ReferenceFootnotes CKEditor 5 plugin. * * Provides: * - A toolbar button that inserts a element. * - Double-click editing of existing elements. * - Simple browser-prompt based dialog for editing attributes. * * This keeps the implementation simple and reliable in Drupal's environment. * If you later want a full ContextualBalloon-based dialog, this is the place * to extend. */ export default class ReferenceFootnotes extends Plugin { static get requires() { return [ Widget ]; } static get pluginName() { return 'ReferenceFootnotes'; } init() { const editor = this.editor; // --- SCHEMA ------------------------------------------------- // Model element: text editor.model.schema.register('fn', { allowWhere: '$text', allowContentOf: '$text', isInline: true, isObject: true, allowAttributes: [ 'value', 'page', 'reference' ] }); // --- MODEL → VIEW (editing) -------------------------------- // Show an inline widget with a class that you can style with an icon. editor.conversion.for('editingDowncast').elementToElement({ model: 'fn', view: (modelElement, { writer }) => { const viewElement = writer.createContainerElement('span', { class: 'reference-footnote', 'data-value': modelElement.getAttribute('value') || '', 'data-reference': modelElement.getAttribute('reference') || '', 'data-page': modelElement.getAttribute('page') || '' }); return toWidget(viewElement, writer, { label: editor.t('Reference footnote') }); } }); // --- MODEL → VIEW (data/HTML) ------------------------------- // Persist as TEXT in the saved HTML. editor.conversion.for('dataDowncast').elementToElement({ model: 'fn', view: (modelElement, { writer }) => { const attrs = { value: modelElement.getAttribute('value') || '', reference: modelElement.getAttribute('reference') || '', page: modelElement.getAttribute('page') || '' }; const fnElement = writer.createContainerElement('fn', attrs); // Put inner text into the element. const firstChild = modelElement.getChild(0); const textData = firstChild && firstChild.is( 'text' ) ? firstChild.data : ''; if (textData) { const textNode = writer.createText(textData); writer.insert(textNode, fnElement, 0); } return fnElement; } }); // --- VIEW → MODEL (upcast) --------------------------------- // Convert tags back into model elements. editor.conversion.for('upcast').elementToElement({ view: { name: 'fn' }, model: (viewElement, { writer }) => { return writer.createElement('fn', { value: viewElement.getAttribute('value') || '', reference: viewElement.getAttribute('reference') || '', page: viewElement.getAttribute('page') || '' }); } }); // --- TOOLBAR BUTTON ----------------------------------------- editor.ui.componentFactory.add('referenceFootnotes', locale => { const view = new ButtonView(locale); view.set({ label: editor.t('Add reference footnote'), icon, tooltip: true }); view.on('execute', () => { this.openFootnoteDialog(null); }); return view; }); // --- DOUBLE CLICK HANDLER ----------------------------------- editor.editing.view.document.on('dblclick', (evt, data) => { const viewElement = data.target; const modelElement = editor.editing.mapper.toModelElement(viewElement); if (modelElement && modelElement.name === 'fn') { this.openFootnoteDialog(modelElement); } }); } /** * Opens a simple "dialog" (browser prompts) to create or edit a footnote. * * @param {?module:engine/model/element~Element} existingFn * The existing model element, or null for a new one. */ openFootnoteDialog(existingFn) { const editor = this.editor; // --- Read existing values (if editing) ---------------------- let currentText = ''; let currentValue = ''; let currentReference = ''; let currentPage = ''; if (existingFn) { const firstChild = existingFn.getChild(0); currentText = firstChild && firstChild.is('text') ? firstChild.data : ''; currentValue = existingFn.getAttribute('value') || ''; currentReference = existingFn.getAttribute('reference') || ''; currentPage = existingFn.getAttribute('page') || ''; } // --- Simple prompts for now -------------------------------- const text = window.prompt(editor.t('Footnote text:'), currentText || ''); if (text === null) { return; // user cancelled } const value = window.prompt(editor.t('Footnote value (e.g. 1, 2, 3):'), currentValue || ''); if (value === null) { return; } const reference = window.prompt(editor.t('Reference (optional):'), currentReference || ''); if (reference === null) { return; } const page = window.prompt(editor.t('Page (optional):'), currentPage || ''); if (page === null) { return; } // --- Write changes to the model ----------------------------- editor.model.change(writer => { if (!existingFn) { // Insert a brand new element at the selection. const fnElement = writer.createElement('fn', { value: value, reference: reference, page: page }); // Insert the element and then its visible text. editor.model.insertObject(fnElement, editor.model.document.selection); writer.insertText(text, fnElement, 0); } else { // Update attributes. writer.setAttribute('value', value, existingFn); writer.setAttribute('reference', reference, existingFn); writer.setAttribute('page', page, existingFn); // Replace inner text. const children = Array.from(existingFn.getChildren()); for (const child of children) { writer.remove(child); } writer.insertText(text, existingFn, 0); } }); } }