From 5a299a7efe79a370f1a0e5cbf62c02e73986f89c Mon Sep 17 00:00:00 2001
From: Bauke
Date: Tue, 12 Nov 2019 15:01:31 +0100
Subject: [PATCH] Feature: Add Markdown snippets toolbar (fixes #12)
---
source/assets/manifest.json | 6 +-
source/html/options.html | 25 +++
source/scss/scripts/markdown-toolbar.scss | 12 ++
source/ts/scripts/markdown-toolbar.ts | 219 ++++++++++++++++++++++
source/ts/utilities.ts | 2 +
5 files changed, 262 insertions(+), 2 deletions(-)
create mode 100644 source/scss/scripts/markdown-toolbar.scss
create mode 100644 source/ts/scripts/markdown-toolbar.ts
diff --git a/source/assets/manifest.json b/source/assets/manifest.json
index 91396fe..5321919 100644
--- a/source/assets/manifest.json
+++ b/source/assets/manifest.json
@@ -39,12 +39,14 @@
"css": [
"../scss/scripts/jump-to-new-comment.scss",
"../scss/scripts/back-to-top.scss",
- "../scss/scripts/user-labels.scss"
+ "../scss/scripts/user-labels.scss",
+ "../scss/scripts/markdown-toolbar.scss"
],
"js": [
"../ts/scripts/jump-to-new-comment.ts",
"../ts/scripts/back-to-top.ts",
- "../ts/scripts/user-labels.ts"
+ "../ts/scripts/user-labels.ts",
+ "../ts/scripts/markdown-toolbar.ts"
]
}
],
diff --git a/source/html/options.html b/source/html/options.html
index 4b0b775..a86d0a1 100644
--- a/source/html/options.html
+++ b/source/html/options.html
@@ -29,6 +29,9 @@
+
+ Markdown Toolbar
+
User Labels
@@ -68,6 +71,28 @@
+
User Labels
diff --git a/source/scss/scripts/markdown-toolbar.scss b/source/scss/scripts/markdown-toolbar.scss
new file mode 100644
index 0000000..ed296f3
--- /dev/null
+++ b/source/scss/scripts/markdown-toolbar.scss
@@ -0,0 +1,12 @@
+.trx-toolbar {
+ > header {
+ justify-content: initial;
+
+ > select {
+ font-size: 0.6rem;
+ margin-left: 4px;
+ margin-right: auto;
+ width: auto;
+ }
+ }
+}
diff --git a/source/ts/scripts/markdown-toolbar.ts b/source/ts/scripts/markdown-toolbar.ts
new file mode 100644
index 0000000..5d02441
--- /dev/null
+++ b/source/ts/scripts/markdown-toolbar.ts
@@ -0,0 +1,219 @@
+import {Settings, getSettings, createElementFromString} from '../utilities';
+
+const markdownSnippets: MarkdownSnippet[] = [
+ {
+ dropdown: false,
+ index: -1,
+ markdown: '[$]()',
+ name: 'Link'
+ },
+ {
+ dropdown: false,
+ index: -1,
+ markdown: '```\n$\n```',
+ name: 'Code'
+ },
+ {
+ dropdown: false,
+ index: -1,
+ markdown: '~~$~~',
+ name: 'Strikethrough'
+ },
+ {
+ dropdown: false,
+ index: -1,
+ markdown:
+ '\nClick to expand spoiler. \n\n$\n ',
+ name: 'Spoilerbox'
+ },
+ {
+ dropdown: true,
+ index: -1,
+ markdown: '**$**',
+ name: 'Bold'
+ },
+ {
+ dropdown: true,
+ index: -1,
+ markdown: '\n\n---\n\n$',
+ name: 'Horizontal Divider'
+ },
+ {
+ dropdown: true,
+ index: -1,
+ markdown: '`$`',
+ name: 'Inline Code'
+ },
+ {
+ dropdown: true,
+ index: -1,
+ markdown: '*$*',
+ name: 'Italic'
+ },
+ {
+ dropdown: true,
+ index: -1,
+ markdown: '1. $',
+ name: 'Ordered List'
+ },
+ {
+ dropdown: true,
+ index: -1,
+ markdown: '$ ',
+ name: 'Small'
+ },
+ {
+ dropdown: true,
+ index: -1,
+ markdown: '* $',
+ name: 'Unordered List'
+ }
+];
+
+(async (): Promise => {
+ const settings: Settings = await getSettings();
+ if (!settings.features.markdownToolbar) {
+ return;
+ }
+
+ calculateSnippetIndexes();
+ // Create an observer that will add toolbars whenever something changes.
+ const observer: MutationObserver = new window.MutationObserver((): void => {
+ observer.disconnect();
+ addToolbarToTextareas();
+ startObserver();
+ });
+
+ function startObserver(): void {
+ observer.observe(document, {
+ childList: true,
+ subtree: true
+ });
+ }
+
+ // Run once when the page loads.
+ addToolbarToTextareas();
+ startObserver();
+})();
+
+interface MarkdownSnippet {
+ dropdown: boolean;
+ index: number;
+ markdown: string;
+ name: string;
+}
+
+function addToolbarToTextareas(): void {
+ // Grab all Markdown forms that don't have already have a toolbar (see below).
+ const markdownForms: NodeListOf = document.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 tabMenu: HTMLMenuElement = form.querySelector(
+ '.tab-markdown-mode'
+ ) as HTMLMenuElement;
+ const textarea: HTMLTextAreaElement = form.querySelector(
+ 'textarea[name="markdown"]'
+ ) as HTMLTextAreaElement;
+ const markdownSelect: HTMLSelectElement = createElementFromString(
+ 'Moreā¦ '
+ );
+ for (const snippet of markdownSnippets) {
+ // If the snippet should go in the dropdown, add the `` for it.
+ if (snippet.dropdown) {
+ const snippetOption: HTMLOptionElement = createElementFromString(
+ ` ${snippet.name} `
+ );
+ markdownSelect.insertAdjacentElement('beforeend', snippetOption);
+ continue;
+ }
+
+ // Otherwise, add it the tab menu as a tab item.
+ const tabItem: HTMLLIElement = createElementFromString(
+ `${snippet.name} `
+ );
+ tabItem.addEventListener('click', (event: MouseEvent): void =>
+ insertSnippet(snippet, textarea, event)
+ );
+ tabMenu.insertAdjacentElement('beforeend', tabItem);
+ }
+
+ // When the dropdown value changes, add the snippet.
+ markdownSelect.addEventListener('change', (): void => {
+ const snippet: MarkdownSnippet | undefined = markdownSnippets.find(
+ val => val.name === markdownSelect.value
+ );
+ if (typeof snippet === 'undefined') {
+ return;
+ }
+
+ insertSnippet(snippet, textarea);
+ // Reset the dropdown index so it always displays "More..." and so it's
+ // possible to select the same snippet multiple times.
+ markdownSelect.selectedIndex = 0;
+ });
+
+ // Insert the dropdown after the tab menu.
+ tabMenu.insertAdjacentElement('afterend', markdownSelect);
+ }
+}
+
+function insertSnippet(
+ snippet: MarkdownSnippet,
+ textarea: HTMLTextAreaElement,
+ event?: MouseEvent
+): void {
+ // If insertSnippet is called from a button it will pass through event.
+ // So preventDefault() that when it's defined.
+ if (typeof event !== 'undefined') {
+ event.preventDefault();
+ }
+
+ // 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();
+ const currentSelectionStart: number = textarea.selectionStart;
+ const currentSelectionEnd: number = textarea.selectionEnd;
+ let {markdown} = snippet;
+ let snippetIndex: number = snippet.index;
+ // If text has been selected, change the markdown so it includes what's
+ // been selected.
+ if (currentSelectionStart !== currentSelectionEnd) {
+ markdown =
+ snippet.markdown.slice(0, snippetIndex) +
+ textarea.value.slice(currentSelectionStart, currentSelectionEnd) +
+ snippet.markdown.slice(snippetIndex);
+
+ // A special behavior for the Link snippet so it places the cursor in the
+ // URL part of a Markdown link instead of the text part: "[](here)".
+ if (snippet.name === 'Link') {
+ snippetIndex += 2;
+ }
+ }
+
+ textarea.value =
+ textarea.value.slice(0, currentSelectionStart) +
+ markdown +
+ textarea.value.slice(currentSelectionEnd);
+ textarea.selectionEnd = currentSelectionEnd + snippetIndex;
+}
+
+// This function gets called at the beginning of the script to set the snippet
+// indexes where the dollar sign is and to remove the dollar sign from it.
+// This could be manually done but I figure it's easier to write snippets and
+// have a placeholder for where the cursor is intended to go than to count
+// where the index is manually and figure it out yourself.
+function calculateSnippetIndexes(): void {
+ for (const snippet of markdownSnippets) {
+ const insertIndex: number = snippet.markdown.indexOf('$');
+ const newMarkdown: string = snippet.markdown.replace('$', '');
+ snippet.index = insertIndex;
+ snippet.markdown = newMarkdown;
+ }
+}
diff --git a/source/ts/utilities.ts b/source/ts/utilities.ts
index 90e5ba7..0071329 100644
--- a/source/ts/utilities.ts
+++ b/source/ts/utilities.ts
@@ -19,6 +19,7 @@ export interface Settings {
backToTop: boolean;
debug: boolean;
jumpToNewComment: boolean;
+ markdownToolbar: boolean;
userLabels: boolean;
[index: string]: boolean;
};
@@ -33,6 +34,7 @@ export const defaultSettings: Settings = {
backToTop: true,
debug: false,
jumpToNewComment: true,
+ markdownToolbar: true,
userLabels: true
}
};