commit
e4ea789e9d
14 changed files with 618 additions and 0 deletions
@ -0,0 +1,9 @@
|
||||
reference_footnotes: |
||||
ckeditor5: |
||||
plugins: |
||||
- ReferenceFootnotes |
||||
drupal: |
||||
label: 'Reference Footnotes' |
||||
toolbar_items: |
||||
referenceFootnotes: |
||||
label: 'Reference Footnotes' |
||||
@ -0,0 +1,17 @@
|
||||
bibcite_footnotes_2_footnote_picker: |
||||
ckeditor5: |
||||
plugins: |
||||
- footnotepicker2.FootnotePicker2 |
||||
|
||||
drupal: |
||||
label: 'Footnote/Citation (Test)' |
||||
library: bibcite_footnotes_2/footnote_picker |
||||
admin_library: bibcite_footnotes_2/footnote_picker |
||||
|
||||
toolbar_items: |
||||
footnotePicker2: |
||||
label: 'Footnoe' |
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path d="M6.5 4.5c-.6 0-1 .4-1 1v4.2c0 .6.4 1 1 1h.8v1.7H6.2c-.6 0-1 .4-1 1v.4h3.3c.6 0 1-.4 1-1V10c0-.5-.3-.9-.7-1.1.4-.3.7-.7.7-1.3V5.5c0-.6-.4-1-1-1H6.5z"/><path d="M11 6h7v1.4h-7V6zm0 3h7v1.4h-7V9zm0 3h5v1.4h-5V12z"/></svg>' |
||||
|
||||
elements: |
||||
- <fn> |
||||
@ -0,0 +1,7 @@
|
||||
name: 'Bibcite Footnotes CKEditor 5' |
||||
type: module |
||||
description: 'Provides a CKEditor 5 plugin for inserting and editing reference footnotes.' |
||||
package: 'Custom' |
||||
core_version_requirement: '^10.2 || ^11' |
||||
dependencies: |
||||
- drupal:ckeditor5 |
||||
@ -0,0 +1,19 @@
|
||||
reference_footnotes: |
||||
js: |
||||
js/ckeditor5/reference-footnotes.js: { type: module } |
||||
css: |
||||
theme: |
||||
css/reference-footnotes.css: {} |
||||
|
||||
footnote_picker: |
||||
css: |
||||
theme: |
||||
css/footnote_picker.admin.css: { } |
||||
css/footnote_picker.dialog.css: {} |
||||
js: |
||||
js/ckeditor5_plugins/footnotepicker2/build/footnotepicker2.js: {} |
||||
dependencies: |
||||
- core/ckeditor5 |
||||
- core/drupal |
||||
- core/drupal.dialog |
||||
- core/jquery |
||||
@ -0,0 +1,5 @@
|
||||
/* Admin UI icon for the toolbar configurator. */ |
||||
.ckeditor5-toolbar-button-footnotePicker2 { |
||||
background-image: url("../icons/footnotes.svg"); |
||||
} |
||||
|
||||
@ -0,0 +1,16 @@
|
||||
.reference-footnotes-dialog { padding-top: 4px; } |
||||
.reference-footnotes-dialog .rf-row { margin: 10px 0; } |
||||
.reference-footnotes-dialog .rf-select { min-width: 140px; } |
||||
.reference-footnotes-dialog .rf-textarea { width: 100%; } |
||||
.reference-footnotes-dialog .rf-help { margin: 10px 0 18px; } |
||||
.reference-footnotes-dialog .rf-grid { |
||||
display: grid; |
||||
grid-template-columns: 1fr 1fr; |
||||
gap: 40px; |
||||
align-items: start; |
||||
margin-top: 6px; |
||||
} |
||||
.reference-footnotes-dialog .rf-field .rf-label { display: block; margin-bottom: 6px; } |
||||
.reference-footnotes-dialog .rf-input { width: 100%; max-width: 520px; } |
||||
.reference-footnotes-dialog .rf-input--value { max-width: 220px; } |
||||
.reference-footnotes-dialog .rf-hint { margin-top: 14px; } |
||||
@ -0,0 +1,21 @@
|
||||
/** |
||||
* Styling for the inline reference footnote widget in CKEditor 5. |
||||
* |
||||
* This mimics a "fake object" icon similar to CKEditor 4. |
||||
*/ |
||||
span.reference-footnote { |
||||
display: inline-block; |
||||
width: 16px; |
||||
height: 16px; |
||||
vertical-align: middle; |
||||
background: no-repeat center center; |
||||
background-size: 16px 16px; |
||||
border: 1px solid #ccc; |
||||
border-radius: 2px; |
||||
padding: 0; |
||||
margin: 0 2px; |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
/* You can optionally point this to a PNG if you prefer. |
||||
For now, this is intended to be used with the SVG icon in toolbar only. */ |
||||
@ -0,0 +1,198 @@
|
||||
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); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
@ -0,0 +1,259 @@
|
||||
/** |
||||
* Prebuilt CKEditor 5 plugin bundle for Drupal (sandbox quality). |
||||
* |
||||
* Provides a toolbar button "Footnote" that opens a Drupal dialog asking for: |
||||
* - Reference ID / key (free text for now) |
||||
* - Optional page |
||||
* Then inserts: <fn reference="..." page="..."></fn> |
||||
* |
||||
* Replace this with a proper build + entity autocomplete later. |
||||
*/ |
||||
(function () { |
||||
const w = window; |
||||
|
||||
// Guard: CKEditor5 globals must exist.
|
||||
w.CKEditor5 = w.CKEditor5 || {}; |
||||
w.CKEditor5.footnotepicker2 = w.CKEditor5.footnotepicker2 || {}; |
||||
|
||||
const Plugin = (w.CKEditor5 && w.CKEditor5.core && w.CKEditor5.core.Plugin) || null; |
||||
const ButtonView = (w.CKEditor5 && w.CKEditor5.ui && w.CKEditor5.ui.ButtonView) || null; |
||||
|
||||
|
||||
if (!Plugin || !ButtonView) { |
||||
// eslint-disable-next-line no-console
|
||||
console.warn('[bibcite_footnotes_2] CKEditor5 globals not found; FootnotePicker2 not registered.'); |
||||
return; |
||||
} |
||||
// Sanitize ONLY the footnote body (not the article).
|
||||
// Allows: <strong>, <em>, <a href="..."> and plain text.
|
||||
function sanitizeFootnoteHtml(input) { |
||||
if (!input) return ''; |
||||
|
||||
const allowedTags = new Set(['STRONG', 'EM', 'A']); |
||||
const allowedAttrs = { |
||||
A: new Set(['href', 'title', 'target', 'rel']), |
||||
}; |
||||
|
||||
const template = document.createElement('template'); |
||||
template.innerHTML = input; |
||||
|
||||
const cleanNode = (node) => { |
||||
// Remove comments.
|
||||
if (node.nodeType === Node.COMMENT_NODE) { |
||||
node.remove(); |
||||
return; |
||||
} |
||||
|
||||
// Text nodes are fine.
|
||||
if (node.nodeType === Node.TEXT_NODE) return; |
||||
|
||||
// Element nodes.
|
||||
if (node.nodeType === Node.ELEMENT_NODE) { |
||||
// Strip disallowed elements but keep their text content.
|
||||
if (!allowedTags.has(node.tagName)) { |
||||
const textNode = document.createTextNode(node.textContent || ''); |
||||
node.replaceWith(textNode); |
||||
return; |
||||
} |
||||
|
||||
// Strip disallowed attributes.
|
||||
[...node.attributes].forEach((attr) => { |
||||
const name = attr.name.toLowerCase(); |
||||
const ok = allowedAttrs[node.tagName] && allowedAttrs[node.tagName].has(name); |
||||
if (!ok) node.removeAttribute(attr.name); |
||||
}); |
||||
|
||||
// Extra link safety.
|
||||
if (node.tagName === 'A') { |
||||
const href = node.getAttribute('href') || ''; |
||||
// Allow only http(s), mailto, and relative URLs.
|
||||
if (!/^(https?:|mailto:|\/)/i.test(href)) { |
||||
node.removeAttribute('href'); |
||||
} |
||||
node.setAttribute('rel', 'noopener noreferrer'); |
||||
} |
||||
} |
||||
|
||||
// Recurse.
|
||||
[...node.childNodes].forEach(cleanNode); |
||||
}; |
||||
|
||||
[...template.content.childNodes].forEach(cleanNode); |
||||
return template.innerHTML; |
||||
} |
||||
|
||||
|
||||
function openFootnoteDialog(onSubmit) { |
||||
const $ = w.jQuery; |
||||
const Drupal = w.Drupal; |
||||
|
||||
// Fallback: prompt() if dialog isn't available.
|
||||
if (!Drupal || !Drupal.dialog || !$) { |
||||
const reference = w.prompt('Reference ID (optional):', '') || ''; |
||||
const text = w.prompt('Free-form footnote text (optional):', '') || ''; |
||||
const page = w.prompt('Page(s) (optional):', '') || ''; |
||||
const value = w.prompt('Value (optional):', '') || ''; |
||||
onSubmit({reference: reference.trim(), text: text.trim(), page: page.trim(), value: value.trim()}); |
||||
return; |
||||
} |
||||
|
||||
const $wrapper = $('<div class="reference-footnotes-dialog"></div>'); |
||||
|
||||
// Row 1: reference selector
|
||||
$wrapper.append('<div class="rf-row rf-row--ref"><label class="rf-label"><strong>Reference Footnote item:</strong></label></div>'); |
||||
const $ref = $(` |
||||
<select class="form-select rf-select"> |
||||
<option value="">- None -</option> |
||||
<!-- TODO: populate dynamically / autocomplete later --> |
||||
</select> |
||||
`);
|
||||
$wrapper.find('.rf-row--ref').append($ref); |
||||
|
||||
// Row 2: free-form textarea
|
||||
$wrapper.append('<div class="rf-row rf-row--or"><div class="rf-or"><strong>Or add free-form footnote text :</strong></div></div>'); |
||||
const $text = $('<textarea class="form-textarea rf-textarea" rows="6"></textarea>'); |
||||
$wrapper.append($('<div class="rf-row rf-row--text"></div>').append($text)); |
||||
|
||||
// Helper line
|
||||
$wrapper.append('<div class="rf-help">HTML tags can be used, e.g., <strong>, <em>, <a href="..."></div>'); |
||||
|
||||
// Row 3: pages + value side-by-side
|
||||
const $grid = $('<div class="rf-grid"></div>'); |
||||
|
||||
const $pageWrap = $(` |
||||
<div class="rf-field"> |
||||
<label class="rf-label">Page(s):</label> |
||||
</div> |
||||
`);
|
||||
const $page = $('<input type="text" class="form-text rf-input" />'); |
||||
$pageWrap.append($page); |
||||
|
||||
const $valueWrap = $(` |
||||
<div class="rf-field"> |
||||
<label class="rf-label">Value :</label> |
||||
</div> |
||||
`);
|
||||
const $value = $('<input type="text" class="form-text rf-input rf-input--value" />'); |
||||
$valueWrap.append($value); |
||||
|
||||
$grid.append($pageWrap, $valueWrap); |
||||
$wrapper.append($grid); |
||||
|
||||
// Hint line
|
||||
$wrapper.append('<div class="rf-hint">Leave blank for an automatic sequential reference number, or enter a custom footnote value</div>'); |
||||
|
||||
const dialog = Drupal.dialog($wrapper.get(0), { |
||||
title: 'Reference Footnotes Dialog', |
||||
width: 900, |
||||
buttons: [ |
||||
{ |
||||
text: 'OK', |
||||
classes: 'button button--primary', |
||||
click: function (event) { |
||||
event.preventDefault(); |
||||
console.log('[footnotes] OK clicked'); |
||||
|
||||
const reference = ($ref.val() || '').toString().trim(); |
||||
const text = ($text.val() || '').toString().trim(); |
||||
const page = ($page.val() || '').toString().trim(); |
||||
const value = ($value.val() || '').toString().trim(); |
||||
|
||||
console.log('[footnotes] values', { reference, text, page, value }); |
||||
|
||||
if (!reference && !text) { |
||||
console.warn('[footnotes] blocked: reference + text are empty'); |
||||
$text.trigger('focus'); |
||||
return; |
||||
} |
||||
|
||||
// IMPORTANT: do the work first.
|
||||
console.log('[footnotes] calling onSubmit()'); |
||||
onSubmit({ reference, text, page, value }); |
||||
|
||||
// Then close/destroy, but never let this prevent insertion.
|
||||
try { dialog.close(); } catch (e) { console.warn('[footnotes] dialog.close failed', e); } |
||||
} |
||||
|
||||
}, |
||||
{ |
||||
text: 'Cancel', |
||||
click: function () { |
||||
dialog.close(); |
||||
dialog.destroy(); |
||||
} |
||||
} |
||||
] |
||||
}); |
||||
|
||||
dialog.showModal(); |
||||
setTimeout(() => $ref.trigger('focus'), 0); |
||||
} |
||||
|
||||
|
||||
class FootnotePicker2 extends Plugin { |
||||
init() { |
||||
const editor = this.editor; |
||||
|
||||
editor.ui.componentFactory.add('footnotePicker2', (locale) => { |
||||
const button = new ButtonView(locale); |
||||
|
||||
const footnoteIcon = ` |
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> |
||||
<path d="M7.8 6.6c0-1.1-.9-2-2-2H4.2c-1.1 0-2 .9-2 2v2.3c0 1.1.9 2 2 2h1.1v1.7H3.7c-1 0-1.7.8-1.7 1.7v.3h4.3c1 0 1.7-.8 1.7-1.7V10c0-.7-.4-1.3-1-1.6.5-.4.8-1 .8-1.8V6.6zm10 0c0-1.1-.9-2-2-2h-1.6c-1.1 0-2 .9-2 2v2.3c0 1.1.9 2 2 2h1.1v1.7h-1.7c-1 0-1.7.8-1.7 1.7v.3h4.3c1 0 1.7-.8 1.7-1.7V10c0-.7-.4-1.3-1-1.6.5-.4.8-1 .8-1.8V6.6z"/> |
||||
</svg> |
||||
`;
|
||||
|
||||
button.set({ |
||||
label: 'Footnote', |
||||
icon: footnoteIcon, |
||||
tooltip: 'Insert footnote', |
||||
withText: false, |
||||
}); |
||||
|
||||
button.on('execute', () => { |
||||
openFootnoteDialog(({reference, text: footnoteText, page, value}) => { |
||||
|
||||
const attrs = []; |
||||
if (reference) attrs.push('reference="' + reference.replace(/"/g, '"') + '"'); |
||||
if (page) attrs.push('page="' + page.replace(/"/g, '"') + '"'); |
||||
if (value) attrs.push('value="' + value.replace(/"/g, '"') + '"'); |
||||
|
||||
const inner = footnoteText ? sanitizeFootnoteHtml(footnoteText) : ''; |
||||
const html = '<fn' + (attrs.length ? ' ' + attrs.join(' ') : '') + '>' + inner + '</fn>'; |
||||
|
||||
let viewFragment, modelFragment; |
||||
|
||||
try { |
||||
viewFragment = editor.data.processor.toView(html); |
||||
modelFragment = editor.data.toModel(viewFragment); |
||||
} catch (e) { |
||||
console.error('[footnotes] toModel failed', e); |
||||
return; |
||||
} |
||||
|
||||
console.log('[footnotes] inserting html:', html); |
||||
console.log('[footnotes] modelFragment childCount:', modelFragment ? modelFragment.childCount : null); |
||||
|
||||
|
||||
editor.model.change((writer) => { |
||||
// If <fn> got stripped, modelFragment will be empty.
|
||||
if (!modelFragment || modelFragment.childCount === 0) { |
||||
console.warn('[footnotes] <fn> was stripped; inserting as plain text fallback'); |
||||
editor.model.insertContent(writer.createText(html), editor.model.document.selection); |
||||
return; |
||||
} |
||||
|
||||
editor.model.insertContent(modelFragment, editor.model.document.selection); |
||||
}); |
||||
|
||||
}); |
||||
}); |
||||
|
||||
|
||||
return button; |
||||
}); |
||||
} |
||||
} |
||||
|
||||
w.CKEditor5.footnotepicker2.FootnotePicker2 = FootnotePicker2; |
||||
})(); |
||||
@ -0,0 +1,32 @@
|
||||
import { Plugin } from 'ckeditor5/src/core'; |
||||
import { ButtonView } from 'ckeditor5/src/ui'; |
||||
|
||||
/** |
||||
* Minimal citation picker for Drupal CKEditor5 (sandbox). |
||||
* |
||||
* NOTE: This source file is not used by Drupal unless you build it. |
||||
* The build output lives in ../build/footnotepicker2.js |
||||
*/ |
||||
import { Plugin } from 'ckeditor5/src/core'; |
||||
import { ButtonView } from 'ckeditor5/src/ui'; |
||||
|
||||
export default class FootnotePicker2 extends Plugin { |
||||
init() { |
||||
const editor = this.editor; |
||||
|
||||
editor.ui.componentFactory.add('footnotePicker2', (locale) => { |
||||
const button = new ButtonView(locale); |
||||
button.set({ |
||||
label: 'Footnote', |
||||
withText: true, |
||||
tooltip: 'Insert footnote', |
||||
}); |
||||
|
||||
button.on('execute', () => { |
||||
// Implemented in the built file for now.
|
||||
}); |
||||
|
||||
return button; |
||||
}); |
||||
} |
||||
} |
||||
@ -0,0 +1,19 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\bibcite_footnotes_2\Plugin\CKEditor5Plugin; |
||||
|
||||
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault; |
||||
use Drupal\Core\StringTranslation\TranslatableMarkup; |
||||
|
||||
#[CKEditor5Plugin( |
||||
id: 'reference_footnotes', |
||||
label: new TranslatableMarkup('Reference Footnotes'), |
||||
ckeditor5: 'reference_footnotes', |
||||
library: 'bibcite_footnotes_2/reference_footnotes', |
||||
elements: [ |
||||
'<fn value page reference>', |
||||
], |
||||
)] |
||||
class ReferenceFootnotes extends CKEditor5PluginDefault { |
||||
|
||||
} |
||||
Loading…
Reference in new issue