Added wysiwyg footnotes hack

This commit is contained in:
Dan Brown 2023-05-04 00:41:07 +01:00
commit 1e8f50e355
Signed by: danb
GPG key ID: 46D9F943C24A2EF9

View file

@ -0,0 +1,126 @@
<script>
// Take a footnote anchor and convert it to the HTML that would be expected
// at the bottom of the page in the list of references.
function footnoteToHtml(elem) {
const newWrap = document.createElement('div');
const newAnchor = document.createElement('a');
const sup = document.createElement('sup');
const text = document.createTextNode(' ' + elem.title.trim());
sup.textContent = elem.textContent.trim();
newAnchor.id = elem.getAttribute('href').replace('#', '');
newAnchor.href = '#';
newAnchor.append(sup);
newWrap.append(newAnchor, text);
return newWrap.outerHTML;
}
// Reset the numbering of all footnotes within the editor
function resetFootnoteNumbering(editor) {
const footnotes = editor.dom.select('a[href^="#bkmrk-footnote-"]');
for (let i = 0; i < footnotes.length; i++) {
const footnote = footnotes[i];
const textEl = footnote.querySelector('sup') || footnote;
textEl.textContent = String(i + 1);
}
}
// Update the footnotes list at the bottom of the content.
function updateFootnotes(editor) {
// Filter out existing footnote blocks on parse
const footnoteBlocks = editor.dom.select('body > div.footnotes');
for (const blocks of footnoteBlocks) {
blocks.remove();
}
// Gather our existing footnote references and return if nothing to add
const footnotes = editor.dom.select('a[href^="#bkmrk-footnote-"]');
if (footnotes.length === 0) {
return;
}
// Build and append our footnote block
resetFootnoteNumbering(editor);
const footnoteHtml = [...footnotes].map(f => footnoteToHtml(f));
editor.dom.add(editor.getBody(), 'div', {class: 'footnotes'}, '<hr/>' + footnoteHtml.join('\n'));
}
// Get the current selected footnote (if any)
function getSelectedFootnote(editor) {
return editor.selection.getNode().closest('a[href^="#bkmrk-footnote-"]');
}
// Insert a new footnote element within the editor at cursor position.
function insertFootnote(editor, text) {
const sup = editor.dom.create('sup', {}, '1');
const anchor = editor.dom.create('a', {href: `#bkmrk-footnote-${Date.now()}`, title: text});
anchor.append(sup);
editor.selection.collapse(false);
editor.insertContent(anchor.outerHTML + ' ');
}
function showFootnoteInsertDialog(editor) {
const footnote = getSelectedFootnote(editor);
// Show a custom form dialog window to edit the footnote text/label
const dialog = editor.windowManager.open({
title: 'Edit Footnote',
body: {
type: 'panel',
items: [{type: 'input', name: 'text', label: 'Footnote Label/Text'}],
},
buttons: [
{type: 'cancel', text: 'Cancel'},
{type: 'submit', text: 'Save', primary: true},
],
onSubmit(api) {
// On submit update or insert a footnote element
const {text} = api.getData();
if (footnote) {
footnote.setAttribute('title', text);
} else {
insertFootnote(editor, text);
editor.execCommand('RemoveFormat');
}
updateFootnotes(editor);
api.close();
},
});
if (footnote) {
dialog.setData({text: footnote.getAttribute('title')});
}
}
// Listen to pre-init event to customize TinyMCE config
window.addEventListener('editor-tinymce::pre-init', event => {
const tinyConfig = event.detail.config;
// Add our custom footnote button to the toolbar
tinyConfig.toolbar = tinyConfig.toolbar.replace('italic ', 'italic footnote ');
});
// Listen to setup event so we customize the editor.
window.addEventListener('editor-tinymce::setup', event => {
// Get a reference to the TinyMCE Editor instance
const editor = event.detail.editor;
// Add our custom footnote button
editor.ui.registry.addToggleButton('footnote', {
icon: 'footnote',
tooltip: 'Add Footnote',
active: false,
onAction() {
showFootnoteInsertDialog(editor);
},
onSetup(api) {
editor.on('NodeChange', event => {
api.setActive(Boolean(getSelectedFootnote(editor)));
});
},
});
// Update footnotes before editor content is fetched
editor.on('BeforeGetContent', () => {
updateFootnotes(editor);
});
});
</script>

View file

@ -0,0 +1,31 @@
+++
title = "WYSIWYG Editor Footnotes"
author = "@ssddanbrown"
date = 2023-05-03T23:00:00Z
updated = 2023-05-03T23:00:00Z
tested = "v23.05"
+++
This hack adds some level of "footnote" support to the WYSIWYG editor.
A new "Footnote" button is added to the toolbar, next to the "Italic" button, that allows you to
insert a new footnote reference. Footnotes will automatically be listed at the bottom of the page content.
The reference numbering is automatic, chronologically from page top to bottom.
New references will change existing numbering if inserted before.
This hack provides significant examples of TinyMCE (The library used for the WYSIWYG) content manipulation and extension.
The code is heavily commented to assist as a helpful example.
For significant alterations, you'll likely want to review the [TinyMCE documentation](https://www.tiny.cloud/docs/tinymce/6/custom-toolbarbuttons/)
to understand the full set of available capabilities and actions within the TinyMCE editor API.
#### Considerations
- This heavily relies on internal methods of TinyMCE, which may change upon any BookStack release as we update the editor libraries.
- All logic is within the WYSIWYG editor, and therefore you won't get the same functionality via the API or other editors.
- The syntax & code used likely won't be cross-compatible with the markdown editor.
- The footnotes list will be generated when content is saved from the editor, so is not updated live but should always be auto-updated before save.
- This has been tested to some degree but there's a reasonable chance of bugs or side affects, since there's quite a lot going on here.
- There's a lot of custom code here. You could instead put this code (without the HTML `<script>` tags) in an external JavaScript file and then just use a single `<script src="/path/to/file.js"></script>` within the custom head setting.
#### Code
{{<hack file="head.html" type="head">}}