1
Fork 0

Feature: Add Markdown snippets toolbar (fixes #12)

This commit is contained in:
Bauke 2019-11-12 15:01:31 +01:00
parent b063e1507c
commit 5a299a7efe
Signed by: Bauke
GPG Key ID: C1C0F29952BCF558
5 changed files with 262 additions and 2 deletions

View File

@ -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"
]
}
],

View File

@ -29,6 +29,9 @@
<a id="jump-to-new-comment-list">
Jump To New Comment
</a>
<a id="markdown-toolbar-list">
Markdown Toolbar
</a>
<a id="user-labels-list">
User Labels
</a>
@ -68,6 +71,28 @@
</p>
</div>
</div>
<div id="markdown-toolbar" class="setting">
<header>
<h2>Markdown Toolbar</h2>
<button id="markdown-toolbar-button"></button>
</header>
<div class="content">
<p>
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
<a href="https://docs.tildes.net/instructions/text-formatting#expandable-sections"
target="_blank" rel="noopener">expandable section</a>/spoilerbox
syntax. If you have text selected, the Markdown will be inserted
around your text.
</p>
<p>
For a list of all snippets,
<a href="https://gitlab.com/tildes-community/tildes-reextended/issues/12"
target="_blank" rel="noopener">see this issue</a>.
</p>
</div>
</div>
<div id="user-labels" class="setting">
<header>
<h2>User Labels</h2>

View File

@ -0,0 +1,12 @@
.trx-toolbar {
> header {
justify-content: initial;
> select {
font-size: 0.6rem;
margin-left: 4px;
margin-right: auto;
width: auto;
}
}
}

View File

@ -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:
'<details>\n<summary>Click to expand spoiler.</summary>\n\n$\n</details>',
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: '<small>$</small>',
name: 'Small'
},
{
dropdown: true,
index: -1,
markdown: '* $',
name: 'Unordered List'
}
];
(async (): Promise<void> => {
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<HTMLDivElement> = 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(
'<select class="form-select"><option>More…</option></select>'
);
for (const snippet of markdownSnippets) {
// If the snippet should go in the dropdown, add the `<option>` for it.
if (snippet.dropdown) {
const snippetOption: HTMLOptionElement = createElementFromString(
`<option value="${snippet.name}">${snippet.name}</option>`
);
markdownSelect.insertAdjacentElement('beforeend', snippetOption);
continue;
}
// Otherwise, add it the tab menu as a tab item.
const tabItem: HTMLLIElement = createElementFromString(
`<li class="tab-item"><button class="btn btn-link">${snippet.name}</button></li>`
);
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;
}
}

View File

@ -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
}
};