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
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); |
|
} |
|
}); |
|
} |
|
}
|
|
|