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

191 lines
5.3 KiB
TypeScript
Raw Normal View History

import {type Value} from "@holllo/webextension-storage";
2023-06-23 10:52:03 +00:00
import {render} from "preact";
import {log, querySelectorAll} from "../../utilities/exports.js";
import {
type MarkdownSnippet,
MarkdownSnippetMarker,
} from "../../storage/exports.js";
export function runMarkdownToolbarFeature(
snippets: Array<Value<MarkdownSnippet>>,
) {
const count = addToolbarsToTextareas(snippets);
if (count > 0) {
log(`Markdown Toolbar: Initialized for ${count} textareas.`);
}
}
function addToolbarsToTextareas(
snippets: Array<Value<MarkdownSnippet>>,
): 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.value.inDropdown)
2023-06-23 10:52:03 +00:00
.map((snippet) => (
<SnippetButton
allSnippets={snippets}
snippet={snippet}
textarea={textarea}
/>
2023-06-23 10:52:03 +00:00
));
const noDropdownSnippets = snippets.length === snippetButtons.length;
// 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);
if (!noDropdownSnippets) {
// And render the dropdown directly after the menu.
const dropdownPlaceholder = document.createElement("div");
const menuParent = menu.parentElement!;
menu.after(dropdownPlaceholder);
render(
<>
<SnippetDropdown allSnippets={snippets} textarea={textarea} />
</>,
menuParent,
dropdownPlaceholder,
);
}
}
return markdownForms.length;
}
type Props = {
allSnippets: Array<Value<MarkdownSnippet>>;
snippet?: Value<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.value.name}
</button>
</li>
2023-06-23 10:52:03 +00:00
);
}
2023-06-23 10:52:03 +00:00
function SnippetDropdown(props: Props) {
const snippets = props.allSnippets;
const options = snippets
?.filter((snippet) => snippet.value.inDropdown)
.map((snippet) => (
<option value={snippet.value.name}>{snippet.value.name}</option>
));
if (options.length === 0) {
return null;
}
const change = (event: Event) => {
event.preventDefault();
const snippet = snippets.find(
(value) => 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 {markdown} = snippet.value;
// Get the marker positions and remove them from the snippet.
let cursorIndex = markdown.indexOf(MarkdownSnippetMarker.Cursor);
markdown = markdown.replace(MarkdownSnippetMarker.Cursor, "");
const selectedCursorIndex = markdown.indexOf(
MarkdownSnippetMarker.SelectedCursor,
);
markdown = markdown.replace(MarkdownSnippetMarker.SelectedCursor, "");
// If we have a Cursor and SelectedCursor in the snippet, and the Cursor is
// placed after the SelectedCursor we have to account for the marker string
// length.
// We don't have to do it in reverse because the Cursor index is taken first
// and the marker string for that is removed before the SelectedCursor index
// is taken.
if (
cursorIndex !== -1 &&
selectedCursorIndex !== -1 &&
cursorIndex > selectedCursorIndex
) {
cursorIndex -= MarkdownSnippetMarker.SelectedCursor.length;
}
if (cursorIndex === -1) {
cursorIndex = 0;
}
let cursorPosition = cursorIndex;
const snippetLength = markdown.length;
// If any text has been selected, include it.
if (selectionStart !== selectionEnd) {
markdown =
markdown.slice(0, cursorIndex) +
textarea.value.slice(selectionStart, selectionEnd) +
markdown.slice(cursorIndex);
cursorPosition =
selectedCursorIndex === -1 ? cursorIndex : selectedCursorIndex;
}
textarea.value =
textarea.value.slice(0, selectionStart) +
markdown +
textarea.value.slice(selectionEnd);
if (cursorPosition === 0) {
// If no <cursor> marker was used in the snippet, then put the cursor at the
// end of the snippet.
cursorPosition = snippetLength;
}
textarea.selectionEnd = selectionEnd + cursorPosition;
}