mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-12-02 15:48:00 +01:00
Convert markdown toolbar logic to TypeScript
Additionally, these changes contain bugfix for the "Escape" button throwing an error if nothing is selected.
This commit is contained in:
parent
073ca2881b
commit
fee1c3e656
1 changed files with 82 additions and 26 deletions
|
@ -4,7 +4,25 @@
|
||||||
|
|
||||||
import { $, $$ } from './utils/dom';
|
import { $, $$ } from './utils/dom';
|
||||||
|
|
||||||
const markdownSyntax = {
|
// List of options provided to the syntax handler function.
|
||||||
|
type SyntaxHandlerOptions = {
|
||||||
|
prefix: string;
|
||||||
|
shortcutKeyCode: number;
|
||||||
|
suffix: string;
|
||||||
|
prefixMultiline: string;
|
||||||
|
suffixMultiline: string;
|
||||||
|
singleWrap: boolean;
|
||||||
|
escapeChar: string;
|
||||||
|
image: boolean;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SyntaxHandler = {
|
||||||
|
action: (textarea: HTMLTextAreaElement, options: Partial<SyntaxHandlerOptions>) => void;
|
||||||
|
options: Partial<SyntaxHandlerOptions>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const markdownSyntax: Record<string, SyntaxHandler> = {
|
||||||
bold: {
|
bold: {
|
||||||
action: wrapSelection,
|
action: wrapSelection,
|
||||||
options: { prefix: '**', shortcutKeyCode: 66 },
|
options: { prefix: '**', shortcutKeyCode: 66 },
|
||||||
|
@ -62,14 +80,22 @@ const markdownSyntax = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSelections(textarea, linesOnly = false) {
|
type SelectionResult = {
|
||||||
|
processLinesOnly: boolean;
|
||||||
|
selectedText: string;
|
||||||
|
beforeSelection: string;
|
||||||
|
afterSelection: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSelections(textarea: HTMLTextAreaElement, linesOnly: RegExp | boolean = false): SelectionResult {
|
||||||
let { selectionStart, selectionEnd } = textarea,
|
let { selectionStart, selectionEnd } = textarea,
|
||||||
selection = textarea.value.substring(selectionStart, selectionEnd),
|
selection = textarea.value.substring(selectionStart, selectionEnd),
|
||||||
leadingSpace = '',
|
leadingSpace = '',
|
||||||
trailingSpace = '',
|
trailingSpace = '',
|
||||||
caret;
|
caret: number;
|
||||||
|
|
||||||
const processLinesOnly = linesOnly instanceof RegExp ? linesOnly.test(selection) : linesOnly;
|
const processLinesOnly = linesOnly instanceof RegExp ? linesOnly.test(selection) : linesOnly;
|
||||||
|
|
||||||
if (processLinesOnly) {
|
if (processLinesOnly) {
|
||||||
const explorer = /\n/g;
|
const explorer = /\n/g;
|
||||||
let startNewlineIndex = 0,
|
let startNewlineIndex = 0,
|
||||||
|
@ -119,7 +145,18 @@ function getSelections(textarea, linesOnly = false) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformSelection(textarea, transformer, eachLine) {
|
type TransformResult = {
|
||||||
|
newText: string;
|
||||||
|
caretOffset: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TransformCallback = (selectedText: string, processLinesOnly: boolean) => TransformResult;
|
||||||
|
|
||||||
|
function transformSelection(
|
||||||
|
textarea: HTMLTextAreaElement,
|
||||||
|
transformer: TransformCallback,
|
||||||
|
eachLine: RegExp | boolean = false,
|
||||||
|
) {
|
||||||
const { selectedText, beforeSelection, afterSelection, processLinesOnly } = getSelections(textarea, eachLine),
|
const { selectedText, beforeSelection, afterSelection, processLinesOnly } = getSelections(textarea, eachLine),
|
||||||
// For long comments, record scrollbar position to restore it later
|
// For long comments, record scrollbar position to restore it later
|
||||||
{ scrollTop } = textarea;
|
{ scrollTop } = textarea;
|
||||||
|
@ -140,7 +177,7 @@ function transformSelection(textarea, transformer, eachLine) {
|
||||||
textarea.dispatchEvent(new Event('change'));
|
textarea.dispatchEvent(new Event('change'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertLink(textarea, options) {
|
function insertLink(textarea: HTMLTextAreaElement, options: Partial<SyntaxHandlerOptions>) {
|
||||||
let hyperlink = window.prompt(options.image ? 'Image link:' : 'Link:');
|
let hyperlink = window.prompt(options.image ? 'Image link:' : 'Link:');
|
||||||
if (!hyperlink || hyperlink === '') return;
|
if (!hyperlink || hyperlink === '') return;
|
||||||
|
|
||||||
|
@ -155,10 +192,11 @@ function insertLink(textarea, options) {
|
||||||
wrapSelection(textarea, { prefix, suffix });
|
wrapSelection(textarea, { prefix, suffix });
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapSelection(textarea, options) {
|
function wrapSelection(textarea: HTMLTextAreaElement, options: Partial<SyntaxHandlerOptions>) {
|
||||||
transformSelection(textarea, selectedText => {
|
transformSelection(textarea, (selectedText: string): TransformResult => {
|
||||||
const { text = selectedText, prefix = '', suffix = options.prefix } = options,
|
const { text = selectedText, prefix = '', suffix = options.prefix } = options,
|
||||||
emptyText = text === '';
|
emptyText = text === '';
|
||||||
|
|
||||||
let newText = text;
|
let newText = text;
|
||||||
|
|
||||||
if (!emptyText) {
|
if (!emptyText) {
|
||||||
|
@ -176,10 +214,14 @@ function wrapSelection(textarea, options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapLines(textarea, options, eachLine = true) {
|
function wrapLines(
|
||||||
|
textarea: HTMLTextAreaElement,
|
||||||
|
options: Partial<SyntaxHandlerOptions>,
|
||||||
|
eachLine: RegExp | boolean = true,
|
||||||
|
) {
|
||||||
transformSelection(
|
transformSelection(
|
||||||
textarea,
|
textarea,
|
||||||
(selectedText, processLinesOnly) => {
|
(selectedText: string, processLinesOnly: boolean): TransformResult => {
|
||||||
const { text = selectedText, singleWrap = false } = options,
|
const { text = selectedText, singleWrap = false } = options,
|
||||||
prefix = (processLinesOnly && options.prefixMultiline) || options.prefix || '',
|
prefix = (processLinesOnly && options.prefixMultiline) || options.prefix || '',
|
||||||
suffix = (processLinesOnly && options.suffixMultiline) || options.suffix || '',
|
suffix = (processLinesOnly && options.suffixMultiline) || options.suffix || '',
|
||||||
|
@ -200,16 +242,22 @@ function wrapLines(textarea, options, eachLine = true) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapSelectionOrLines(textarea, options) {
|
function wrapSelectionOrLines(textarea: HTMLTextAreaElement, options: Partial<SyntaxHandlerOptions>) {
|
||||||
wrapLines(textarea, options, /\n/);
|
wrapLines(textarea, options, /\n/);
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeSelection(textarea, options) {
|
function escapeSelection(textarea: HTMLTextAreaElement, options: Partial<SyntaxHandlerOptions>) {
|
||||||
transformSelection(textarea, selectedText => {
|
transformSelection(textarea, (selectedText: string): TransformResult => {
|
||||||
const { text = selectedText } = options,
|
const { text = selectedText } = options,
|
||||||
emptyText = text === '';
|
emptyText = text === '';
|
||||||
|
|
||||||
if (emptyText) return;
|
// Even if there is nothing to escape, we still need to return the result, otherwise the error would be thrown.
|
||||||
|
if (emptyText) {
|
||||||
|
return {
|
||||||
|
newText: text,
|
||||||
|
caretOffset: text.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const newText = text.replace(/([*_[\]()^`%\\~<>#|])/g, '\\$1');
|
const newText = text.replace(/([*_[\]()^`%\\~<>#|])/g, '\\$1');
|
||||||
|
|
||||||
|
@ -220,22 +268,28 @@ function escapeSelection(textarea, options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function clickHandler(event) {
|
function clickHandler(event: MouseEvent) {
|
||||||
const button = event.target.closest('.communication__toolbar__button');
|
if (!(event.target instanceof HTMLElement)) return;
|
||||||
if (!button) return;
|
|
||||||
const toolbar = button.closest('.communication__toolbar'),
|
const button = event.target?.closest<HTMLElement>('.communication__toolbar__button');
|
||||||
// There may be multiple toolbars present on the page,
|
const toolbar = button?.closest<HTMLElement>('.communication__toolbar');
|
||||||
// in the case of image pages with description edit active
|
|
||||||
// we target the textarea that shares the same parent as the toolbar
|
if (!button || !toolbar?.parentElement) return;
|
||||||
textarea = $('.js-toolbar-input', toolbar.parentNode),
|
|
||||||
|
// There may be multiple toolbars present on the page,
|
||||||
|
// in the case of image pages with description edit active
|
||||||
|
// we target the textarea that shares the same parent as the toolbar
|
||||||
|
const textarea = $<HTMLTextAreaElement>('.js-toolbar-input', toolbar.parentElement),
|
||||||
id = button.dataset.syntaxId;
|
id = button.dataset.syntaxId;
|
||||||
|
|
||||||
|
if (!textarea || !id) return;
|
||||||
|
|
||||||
markdownSyntax[id].action(textarea, markdownSyntax[id].options);
|
markdownSyntax[id].action(textarea, markdownSyntax[id].options);
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function canAcceptShortcut(event) {
|
function canAcceptShortcut(event: KeyboardEvent): boolean {
|
||||||
let ctrl, otherModifier;
|
let ctrl: boolean, otherModifier: boolean;
|
||||||
|
|
||||||
switch (window.navigator.platform) {
|
switch (window.navigator.platform) {
|
||||||
case 'MacIntel':
|
case 'MacIntel':
|
||||||
|
@ -251,7 +305,7 @@ function canAcceptShortcut(event) {
|
||||||
return ctrl && !otherModifier;
|
return ctrl && !otherModifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortcutHandler(event) {
|
function shortcutHandler(event: KeyboardEvent) {
|
||||||
if (!canAcceptShortcut(event)) {
|
if (!canAcceptShortcut(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -259,6 +313,8 @@ function shortcutHandler(event) {
|
||||||
const textarea = event.target,
|
const textarea = event.target,
|
||||||
keyCode = event.keyCode;
|
keyCode = event.keyCode;
|
||||||
|
|
||||||
|
if (!(textarea instanceof HTMLTextAreaElement)) return;
|
||||||
|
|
||||||
for (const id in markdownSyntax) {
|
for (const id in markdownSyntax) {
|
||||||
if (keyCode === markdownSyntax[id].options.shortcutKeyCode) {
|
if (keyCode === markdownSyntax[id].options.shortcutKeyCode) {
|
||||||
markdownSyntax[id].action(textarea, markdownSyntax[id].options);
|
markdownSyntax[id].action(textarea, markdownSyntax[id].options);
|
||||||
|
@ -268,10 +324,10 @@ function shortcutHandler(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupToolbar() {
|
function setupToolbar() {
|
||||||
$$('.communication__toolbar').forEach(toolbar => {
|
$$<HTMLElement>('.communication__toolbar').forEach(toolbar => {
|
||||||
toolbar.addEventListener('click', clickHandler);
|
toolbar.addEventListener('click', clickHandler);
|
||||||
});
|
});
|
||||||
$$('.js-toolbar-input').forEach(textarea => {
|
$$<HTMLTextAreaElement>('.js-toolbar-input').forEach(textarea => {
|
||||||
textarea.addEventListener('keydown', shortcutHandler);
|
textarea.addEventListener('keydown', shortcutHandler);
|
||||||
});
|
});
|
||||||
}
|
}
|
Loading…
Reference in a new issue