Bibcite footnotes modified for CKEditor 5
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

198 lines
7.2 KiB

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 <fn> element.
* - Double-click editing of existing <fn> 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: <fn value="" page="" reference="">text</fn>
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 <fn ...>TEXT</fn> 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 <fn> 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 <fn> 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 <fn> 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 <fn> 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);
}
});
}
}