201 lines
4.6 KiB
TypeScript
201 lines
4.6 KiB
TypeScript
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:
|
|
"<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() {
|
|
const count = addToolbarsToTextareas();
|
|
log(`Markdown Toolbar: Initialized for ${count} textareas.`);
|
|
}
|
|
|
|
function addToolbarsToTextareas(): number {
|
|
// 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 0;
|
|
}
|
|
|
|
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<HTMLTextAreaElement>(
|
|
'textarea[name="markdown"]',
|
|
)!;
|
|
|
|
const snippetButtons = snippets
|
|
.filter((snippet) => !snippet.dropdown)
|
|
.map((snippet) => (
|
|
<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(
|
|
<SnippetDropdown textarea={textarea} />,
|
|
menuParent,
|
|
dropdownPlaceholder,
|
|
);
|
|
}
|
|
|
|
return markdownForms.length;
|
|
}
|
|
|
|
type Props = {
|
|
snippet?: MarkdownSnippet;
|
|
textarea: HTMLTextAreaElement;
|
|
};
|
|
|
|
function SnippetButton(props: Required<Props>) {
|
|
const click = (event: MouseEvent) => {
|
|
event.preventDefault();
|
|
insertSnippet(props);
|
|
};
|
|
|
|
return (
|
|
<li class="tab-item">
|
|
<button class="btn btn-link" onClick={click}>
|
|
{props.snippet.name}
|
|
</button>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
function SnippetDropdown(props: Props) {
|
|
const options = snippets.map((snippet) => (
|
|
<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 (
|
|
<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;
|
|
}
|