import {html} from 'htm/preact'; import {render} from 'preact'; import {log, querySelectorAll} from '../utilities/exports.js'; type MarkdownSnippet = { dropdown: boolean; index: number; markdown: string; name: string; }; const snippets: MarkdownSnippet[] = [ { dropdown: false, markdown: '[<>]()', name: 'Link', }, { dropdown: false, markdown: '```\n<>\n```', name: 'Code', }, { dropdown: false, markdown: '~~<>~~', name: 'Strikethrough', }, { dropdown: false, markdown: '
\nClick to expand spoiler.\n\n<>\n
', name: 'Spoilerbox', }, { dropdown: true, markdown: '**<>**', name: 'Bold', }, { dropdown: true, markdown: '\n\n---\n\n<>', name: 'Horizontal Divider', }, { dropdown: true, markdown: '`<>`', name: 'Inline Code', }, { dropdown: true, markdown: '*<>*', name: 'Italic', }, { dropdown: true, markdown: '1. <>', name: 'Ordered List', }, { dropdown: true, markdown: '<>', name: 'Small', }, { dropdown: true, markdown: '* <>', name: 'Unordered List', }, ].map(({dropdown, markdown, name}) => ({ dropdown, name, index: markdown.indexOf('<>'), markdown: markdown.replace(/<>/, ''), })); export function runMarkdownToolbarFeature() { // Create an observer that will add toolbars whenever // a new Markdown form is created (like when clicking Reply). const observer = new window.MutationObserver(() => { observer.disconnect(); addToolbarsToTextareas(); startObserver(); }); function startObserver() { observer.observe(document, { childList: true, subtree: true, }); } // Run once when the page loads. addToolbarsToTextareas(); startObserver(); log('Markdown Toolbar: Initialized.'); } function addToolbarsToTextareas() { // Grab all Markdown forms that don't have already have a toolbar. const markdownForms = querySelectorAll('.form-markdown:not(.trx-toolbar)'); if (markdownForms.length === 0) { return; } for (const form of markdownForms) { // Add `trx-toolbar` to indicate this Markdown form already has the toolbar. form.classList.add('trx-toolbar'); const menu = form.querySelector('.tab-markdown-mode')!; const textarea = form.querySelector( 'textarea[name="markdown"]', )!; const snippetButtons = snippets .filter((snippet) => !snippet.dropdown) .map( (snippet) => html`<${snippetButton} snippet=${snippet} textarea=${textarea} />`, ); // Render the buttons inside the tab menu so they appear // next to the Edit and Preview buttons. const menuPlaceholder = document.createElement('div'); menu.append(menuPlaceholder); render(snippetButtons, menu, menuPlaceholder); // And render the dropdown directly after the menu. const dropdownPlaceholder = document.createElement('div'); const menuParent = menu.parentElement!; menu.after(dropdownPlaceholder); render( html`<${snippetDropdown} textarea=${textarea} />`, menuParent, dropdownPlaceholder, ); } } type Props = { snippet?: MarkdownSnippet; textarea: HTMLTextAreaElement; }; function snippetButton(props: Required): TRXComponent { const click = (event: MouseEvent) => { event.preventDefault(); insertSnippet(props); }; return html`
  • `; } function snippetDropdown(props: Props): TRXComponent { const options = snippets.map( (snippet) => html``, ); const change = (event: Event) => { event.preventDefault(); const snippet = snippets.find( (value) => value.name === (event.target as HTMLSelectElement).value, )!; insertSnippet({ ...props, snippet, }); (event.target as HTMLSelectElement).selectedIndex = 0; }; return html` `; } function insertSnippet(props: Required) { const {textarea, snippet} = props; const {selectionStart, selectionEnd} = textarea; // Since you have to press a button or go into a dropdown to click on a // snippet, the textarea won't be focused anymore. So focus it again. textarea.focus(); let {index, markdown} = snippet; // If any text has been selected, include it. if (selectionStart !== selectionEnd) { markdown = markdown.slice(0, index) + textarea.value.slice(selectionStart, selectionEnd) + markdown.slice(index); // Change the index when the Link snippet is used so the cursor ends up // in the URL part of the Markdown: "[existing text](cursor here)". if (snippet.name === 'Link') { index += 2; } } textarea.value = textarea.value.slice(0, selectionStart) + markdown + textarea.value.slice(selectionEnd); textarea.selectionEnd = selectionEnd + index; }