forked from bookstack/hacks
Added wysiwyg footnotes hack
This commit is contained in:
parent bd6eb19bce
commit 1e8f50e355
2 changed files with 157 additions and 0 deletions
126 content/wysiwyg-footnotes/head.html Normal file
126
content/wysiwyg-footnotes/head.html Normal 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> |
31 content/wysiwyg-footnotes/index.md Normal file
31
content/wysiwyg-footnotes/index.md Normal 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">}} |
Loading…
Add table
Add a link
Reference in a new issue