1
Fork 0
tildes-reextended/source/content-scripts/features/markdown-toolbar.tsx

201 lines
4.6 KiB
TypeScript
Raw Normal View History

2023-06-23 10:52:03 +00:00
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,
2023-06-23 10:52:03 +00:00
markdown: "[<>]()",
name: "Link",
},
{
dropdown: false,
2023-06-23 10:52:03 +00:00
markdown: "```\n<>\n```",
name: "Code",
},
{
dropdown: false,
2023-06-23 10:52:03 +00:00
markdown: "~~<>~~",
name: "Strikethrough",
},
{
dropdown: false,
markdown:
2023-06-23 10:52:03 +00:00
"<details>\n<summary>Click to expand spoiler.</summary>\n\n<>\n</details>",
name: "Spoilerbox",
},
{
dropdown: true,
2023-06-23 10:52:03 +00:00
markdown: "**<>**",
name: "Bold",
},
{
dropdown: true,
2023-06-23 10:52:03 +00:00
markdown: "\n\n---\n\n<>",
name: "Horizontal Divider",
},
{
dropdown: true,
2023-06-23 10:52:03 +00:00
markdown: "`<>`",
name: "Inline Code",
},
{
dropdown: true,
2023-06-23 10:52:03 +00:00
markdown: "*<>*",
name: "Italic",
},
{
dropdown: true,
2023-06-23 10:52:03 +00:00
markdown: "1. <>",
name: "Ordered List",
},
{
dropdown: true,
2023-06-23 10:52:03 +00:00
markdown: "<small><></small>",
name: "Small",
},
{
dropdown: true,
2023-06-23 10:52:03 +00:00
markdown: "* <>",
name: "Unordered List",
},
].map(({dropdown, markdown, name}) => ({
dropdown,
name,
2023-06-23 10:52:03 +00:00
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.
2023-06-23 10:52:03 +00:00
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.
2023-06-23 10:52:03 +00:00
form.classList.add("trx-toolbar");
2023-06-23 10:52:03 +00:00
const menu = form.querySelector<HTMLElement>(".tab-markdown-mode")!;
const textarea = form.querySelector<HTMLTextAreaElement>(
'textarea[name="markdown"]',
)!;
const snippetButtons = snippets
.filter((snippet) => !snippet.dropdown)
2023-06-23 10:52:03 +00:00
.map((snippet) => (
<SnippetButton snippet={snippet} textarea={textarea} />
));
// Render the buttons inside the tab menu so they appear
// next to the Edit and Preview buttons.
2023-06-23 10:52:03 +00:00
const menuPlaceholder = document.createElement("div");
menu.append(menuPlaceholder);
render(snippetButtons, menu, menuPlaceholder);
// And render the dropdown directly after the menu.
2023-06-23 10:52:03 +00:00
const dropdownPlaceholder = document.createElement("div");
const menuParent = menu.parentElement!;
menu.after(dropdownPlaceholder);
render(
2023-06-23 10:52:03 +00:00
<SnippetDropdown textarea={textarea} />,
menuParent,
dropdownPlaceholder,
);
}
return markdownForms.length;
}
type Props = {
snippet?: MarkdownSnippet;
textarea: HTMLTextAreaElement;
};
2023-06-23 10:52:03 +00:00
function SnippetButton(props: Required<Props>) {
const click = (event: MouseEvent) => {
event.preventDefault();
insertSnippet(props);
};
2023-06-23 10:52:03 +00:00
return (
<li class="tab-item">
2023-06-23 10:52:03 +00:00
<button class="btn btn-link" onClick={click}>
{props.snippet.name}
</button>
</li>
2023-06-23 10:52:03 +00:00
);
}
2023-06-23 10:52:03 +00:00
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;
};
2023-06-23 10:52:03 +00:00
return (
<select class="form-select" onChange={change}>
<option>More</option>
2023-06-23 10:52:03 +00:00
{options}
</select>
2023-06-23 10:52:03 +00:00
);
}
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)".
2023-06-23 10:52:03 +00:00
if (snippet.name === "Link") {
index += 2;
}
}
textarea.value =
textarea.value.slice(0, selectionStart) +
markdown +
textarea.value.slice(selectionEnd);
textarea.selectionEnd = selectionEnd + index;
}