2 // Take a footnote anchor and convert it to the HTML that would be expected
3 // at the bottom of the page in the list of references.
4 function footnoteToHtml(elem) {
5 const newWrap = document.createElement('div');
6 const newAnchor = document.createElement('a');
7 const sup = document.createElement('sup');
8 const text = document.createTextNode(' ' + elem.title.trim());
9 sup.textContent = elem.textContent.trim();
10 newAnchor.id = elem.getAttribute('href').replace('#', '');
12 newAnchor.append(sup);
13 newWrap.append(newAnchor, text);
14 return newWrap.outerHTML;
17 // Reset the numbering of all footnotes within the editor
18 function resetFootnoteNumbering(editor) {
19 const footnotes = editor.dom.select('a[href^="#bkmrk-footnote-"]');
20 for (let i = 0; i < footnotes.length; i++) {
21 const footnote = footnotes[i];
22 const textEl = footnote.querySelector('sup') || footnote;
23 textEl.textContent = String(i + 1);
27 // Update the footnotes list at the bottom of the content.
28 function updateFootnotes(editor) {
29 // Filter out existing footnote blocks on parse
30 const footnoteBlocks = editor.dom.select('body > div.footnotes');
31 for (const blocks of footnoteBlocks) {
35 // Gather our existing footnote references and return if nothing to add
36 const footnotes = editor.dom.select('a[href^="#bkmrk-footnote-"]');
37 if (footnotes.length === 0) {
41 // Build and append our footnote block
42 resetFootnoteNumbering(editor);
43 const footnoteHtml = [...footnotes].map(f => footnoteToHtml(f));
44 editor.dom.add(editor.getBody(), 'div', {class: 'footnotes'}, '<hr/>' + footnoteHtml.join('\n'));
47 // Get the current selected footnote (if any)
48 function getSelectedFootnote(editor) {
49 return editor.selection.getNode().closest('a[href^="#bkmrk-footnote-"]');
52 // Insert a new footnote element within the editor at cursor position.
53 function insertFootnote(editor, text) {
54 const sup = editor.dom.create('sup', {}, '1');
55 const anchor = editor.dom.create('a', {href: `#bkmrk-footnote-${Date.now()}`, title: text});
57 editor.selection.collapse(false);
58 editor.insertContent(anchor.outerHTML + ' ');
61 function showFootnoteInsertDialog(editor) {
62 const footnote = getSelectedFootnote(editor);
64 // Show a custom form dialog window to edit the footnote text/label
65 const dialog = editor.windowManager.open({
66 title: 'Edit Footnote',
69 items: [{type: 'input', name: 'text', label: 'Footnote Label/Text'}],
72 {type: 'cancel', text: 'Cancel'},
73 {type: 'submit', text: 'Save', primary: true},
76 // On submit update or insert a footnote element
77 const {text} = api.getData();
79 footnote.setAttribute('title', text);
81 insertFootnote(editor, text);
82 editor.execCommand('RemoveFormat');
84 updateFootnotes(editor);
90 dialog.setData({text: footnote.getAttribute('title')});
94 // Listen to pre-init event to customize TinyMCE config
95 window.addEventListener('editor-tinymce::pre-init', event => {
96 const tinyConfig = event.detail.config;
97 // Add our custom footnote button to the toolbar
98 tinyConfig.toolbar = tinyConfig.toolbar.replace('italic ', 'italic footnote ');
101 // Listen to setup event so we customize the editor.
102 window.addEventListener('editor-tinymce::setup', event => {
103 // Get a reference to the TinyMCE Editor instance
104 const editor = event.detail.editor;
106 // Add our custom footnote button
107 editor.ui.registry.addToggleButton('footnote', {
109 tooltip: 'Add Footnote',
112 showFootnoteInsertDialog(editor);
115 editor.on('NodeChange', event => {
116 api.setActive(Boolean(getSelectedFootnote(editor)));
121 // Update footnotes before editor content is fetched
122 editor.on('BeforeGetContent', () => {
123 updateFootnotes(editor);