1
Fork 0
tildes-reextended/source/scripts/markdown-toolbar.ts

215 lines
5.0 KiB
TypeScript
Raw Permalink Normal View History

import {html} from 'htm/preact';
import {render} from 'preact';
import {log, querySelectorAll, TRXComponent} from '..';
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:
'<details>\n<summary>Click to expand spoiler.</summary>\n\n<>\n</details>',
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: '<small><></small>',
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<HTMLElement>('.tab-markdown-mode')!;
const textarea = form.querySelector<HTMLElement>(
'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<Props>): TRXComponent {
const click = (event: MouseEvent) => {
event.preventDefault();
insertSnippet(props);
};
return html`<li class="tab-item">
<button class="btn btn-link" onClick="${click}">
${props.snippet.name}
</button>
</li>`;
}
function snippetDropdown(props: Props): TRXComponent {
const options = snippets.map(
(snippet) => html`<option value="${snippet.name}">${snippet.name}</option>`
);
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`<select class="form-select" onChange=${change}>
<option>More</option>
${options}
</select>`;
}
function insertSnippet(props: Required<Props>) {
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;
}