Feature: Add Markdown snippets toolbar (fixes #12)
This commit is contained in:
parent
b063e1507c
commit
5a299a7efe
|
@ -39,12 +39,14 @@
|
||||||
"css": [
|
"css": [
|
||||||
"../scss/scripts/jump-to-new-comment.scss",
|
"../scss/scripts/jump-to-new-comment.scss",
|
||||||
"../scss/scripts/back-to-top.scss",
|
"../scss/scripts/back-to-top.scss",
|
||||||
"../scss/scripts/user-labels.scss"
|
"../scss/scripts/user-labels.scss",
|
||||||
|
"../scss/scripts/markdown-toolbar.scss"
|
||||||
],
|
],
|
||||||
"js": [
|
"js": [
|
||||||
"../ts/scripts/jump-to-new-comment.ts",
|
"../ts/scripts/jump-to-new-comment.ts",
|
||||||
"../ts/scripts/back-to-top.ts",
|
"../ts/scripts/back-to-top.ts",
|
||||||
"../ts/scripts/user-labels.ts"
|
"../ts/scripts/user-labels.ts",
|
||||||
|
"../ts/scripts/markdown-toolbar.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -29,6 +29,9 @@
|
||||||
<a id="jump-to-new-comment-list">
|
<a id="jump-to-new-comment-list">
|
||||||
Jump To New Comment
|
Jump To New Comment
|
||||||
</a>
|
</a>
|
||||||
|
<a id="markdown-toolbar-list">
|
||||||
|
Markdown Toolbar
|
||||||
|
</a>
|
||||||
<a id="user-labels-list">
|
<a id="user-labels-list">
|
||||||
User Labels
|
User Labels
|
||||||
</a>
|
</a>
|
||||||
|
@ -68,6 +71,28 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div id="user-labels" class="setting">
|
||||||
<header>
|
<header>
|
||||||
<h2>User Labels</h2>
|
<h2>User Labels</h2>
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
.trx-toolbar {
|
||||||
|
> header {
|
||||||
|
justify-content: initial;
|
||||||
|
|
||||||
|
> select {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: auto;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ export interface Settings {
|
||||||
backToTop: boolean;
|
backToTop: boolean;
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
jumpToNewComment: boolean;
|
jumpToNewComment: boolean;
|
||||||
|
markdownToolbar: boolean;
|
||||||
userLabels: boolean;
|
userLabels: boolean;
|
||||||
[index: string]: boolean;
|
[index: string]: boolean;
|
||||||
};
|
};
|
||||||
|
@ -33,6 +34,7 @@ export const defaultSettings: Settings = {
|
||||||
backToTop: true,
|
backToTop: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
jumpToNewComment: true,
|
jumpToNewComment: true,
|
||||||
|
markdownToolbar: true,
|
||||||
userLabels: true
|
userLabels: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue