]> BookStack Code Mirror - hacks/commitdiff
Added wysiwyg footnotes hack
authorDan Brown <redacted>
Wed, 3 May 2023 23:41:07 +0000 (00:41 +0100)
committerDan Brown <redacted>
Wed, 3 May 2023 23:41:07 +0000 (00:41 +0100)
content/wysiwyg-footnotes/head.html [new file with mode: 0644]
content/wysiwyg-footnotes/index.md [new file with mode: 0644]

diff --git a/content/wysiwyg-footnotes/head.html b/content/wysiwyg-footnotes/head.html
new file mode 100644 (file)
index 0000000..1b1e2f6
--- /dev/null
@@ -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>
\ No newline at end of file
diff --git a/content/wysiwyg-footnotes/index.md b/content/wysiwyg-footnotes/index.md
new file mode 100644 (file)
index 0000000..0bbca11
--- /dev/null
@@ -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">}}