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 @@ Jump To New Comment + + Markdown Toolbar + User Labels @@ -68,6 +71,28 @@

+
+
+

Markdown Toolbar

+ +
+
+

+ Adds a toolbar with a selection of Markdown snippets that when + used will insert the according Markdown where your cursor is. + Particularly useful for the + expandable section/spoilerbox + syntax. If you have text selected, the Markdown will be inserted + around your text. +

+

+ For a list of all snippets, + see this issue. +

+
+

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( + '' + ); + for (const snippet of markdownSnippets) { + // If the snippet should go in the dropdown, add the `` + ); + markdownSelect.insertAdjacentElement('beforeend', snippetOption); + continue; + } + + // Otherwise, add it the tab menu as a tab item. + const tabItem: HTMLLIElement = createElementFromString( + `
  • ` + ); + 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 } };