mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-12-05 00:57:59 +01:00
173 lines
5.1 KiB
JavaScript
173 lines
5.1 KiB
JavaScript
|
/**
|
||
|
* Textile toolbar
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
import { $, $$ } from './utils/dom';
|
||
|
|
||
|
const textileSyntax = {
|
||
|
bold: {
|
||
|
action: wrapSelection,
|
||
|
options: { prefix: '*', suffix: '*', shortcutKey: 'b' }
|
||
|
},
|
||
|
italics: {
|
||
|
action: wrapSelection,
|
||
|
options: { prefix: '_', suffix: '_', shortcutKey: 'i' }
|
||
|
},
|
||
|
under: {
|
||
|
action: wrapSelection,
|
||
|
options: { prefix: '+', suffix: '+', shortcutKey: 'u' }
|
||
|
},
|
||
|
spoiler: {
|
||
|
action: wrapSelection,
|
||
|
options: { prefix: '[spoiler]', suffix: '[/spoiler]', shortcutKey: 's' }
|
||
|
},
|
||
|
code: {
|
||
|
action: wrapSelection,
|
||
|
options: { prefix: '@', suffix: '@', shortcutKey: 'e' }
|
||
|
},
|
||
|
strike: {
|
||
|
action: wrapSelection,
|
||
|
options: { prefix: '-', suffix: '-' }
|
||
|
},
|
||
|
superscript: {
|
||
|
action: wrapSelection,
|
||
|
options: { prefix: '^', suffix: '^' }
|
||
|
},
|
||
|
subscript: {
|
||
|
action: wrapSelection,
|
||
|
options: { prefix: '~', suffix: '~' }
|
||
|
},
|
||
|
quote: {
|
||
|
action: wrapSelection,
|
||
|
options: { prefix: '[bq]', suffix: '[/bq]' }
|
||
|
},
|
||
|
link: {
|
||
|
action: insertLink,
|
||
|
options: { prefix: '"', suffix: '":', shortcutKey: 'l' }
|
||
|
},
|
||
|
image: {
|
||
|
action: insertImage,
|
||
|
options: { prefix: '!', suffix: '!', shortcutKey: 'k' }
|
||
|
},
|
||
|
noParse: {
|
||
|
action: wrapSelection,
|
||
|
options: { prefix: '[==', suffix: '==]' }
|
||
|
},
|
||
|
};
|
||
|
|
||
|
function getSelections(textarea) {
|
||
|
let selection = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd),
|
||
|
leadingSpace = '',
|
||
|
trailingSpace = '',
|
||
|
caret;
|
||
|
|
||
|
// Deselect trailing space and line break
|
||
|
for (caret = selection.length - 1; caret > 0; caret--) {
|
||
|
if (selection[caret] !== ' ' && selection[caret] !== '\n') break;
|
||
|
trailingSpace = selection[caret] + trailingSpace;
|
||
|
}
|
||
|
selection = selection.substring(0, caret + 1);
|
||
|
|
||
|
// Deselect leading space and line break
|
||
|
for (caret = 0; caret < selection.length; caret++) {
|
||
|
if (selection[caret] !== ' ' && selection[caret] !== '\n') break;
|
||
|
leadingSpace += selection[caret];
|
||
|
}
|
||
|
selection = selection.substring(caret);
|
||
|
|
||
|
return {
|
||
|
selectedText: selection,
|
||
|
beforeSelection: textarea.value.substring(0, textarea.selectionStart) + leadingSpace,
|
||
|
afterSelection: trailingSpace + textarea.value.substring(textarea.selectionEnd),
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function wrapSelection(textarea, options) {
|
||
|
const { selectedText, beforeSelection, afterSelection } = getSelections(textarea),
|
||
|
{ text = selectedText, prefix = '', suffix = '' } = options,
|
||
|
// For long comments, record scrollbar position to restore it later
|
||
|
scrollTop = textarea.scrollTop,
|
||
|
emptyText = text === '';
|
||
|
|
||
|
let newText = text;
|
||
|
if (!emptyText && prefix[0] !== '[') {
|
||
|
newText = text.replace(/(\n{2,})/g, match => {
|
||
|
return suffix + match + prefix;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
textarea.value = beforeSelection + prefix + newText + suffix + afterSelection;
|
||
|
|
||
|
// If no text were highlighted, place the caret inside
|
||
|
// the formatted section, otherwise place it at the end
|
||
|
if (emptyText) {
|
||
|
textarea.selectionEnd = textarea.value.length - afterSelection.length - suffix.length;
|
||
|
}
|
||
|
else {
|
||
|
textarea.selectionEnd = textarea.value.length - afterSelection.length;
|
||
|
}
|
||
|
textarea.selectionStart = textarea.selectionEnd;
|
||
|
textarea.scrollTop = scrollTop;
|
||
|
}
|
||
|
|
||
|
function insertLink(textarea, options) {
|
||
|
let hyperlink = window.prompt('Link:');
|
||
|
if (!hyperlink || hyperlink === '') return;
|
||
|
|
||
|
// Change on-site link to use relative url
|
||
|
if (hyperlink.startsWith(window.location.origin)) hyperlink = hyperlink.substring(window.location.origin.length);
|
||
|
|
||
|
const prefix = options.prefix,
|
||
|
suffix = options.suffix + hyperlink;
|
||
|
|
||
|
wrapSelection(textarea, { prefix, suffix });
|
||
|
}
|
||
|
|
||
|
function insertImage(textarea, options) {
|
||
|
const hyperlink = window.prompt('Image link:');
|
||
|
const { prefix, suffix } = options;
|
||
|
|
||
|
if (!hyperlink || hyperlink === '') return;
|
||
|
|
||
|
wrapSelection(textarea, { text: hyperlink, prefix, suffix });
|
||
|
}
|
||
|
|
||
|
function clickHandler(event) {
|
||
|
const button = event.target.closest('.communication__toolbar__button');
|
||
|
if (!button) return;
|
||
|
const toolbar = button.closest('.communication__toolbar'),
|
||
|
// 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 toolabr
|
||
|
textarea = $('.js-toolbar-input', toolbar.parentNode),
|
||
|
id = button.dataset.syntaxId;
|
||
|
|
||
|
textileSyntax[id].action(textarea, textileSyntax[id].options);
|
||
|
textarea.focus();
|
||
|
}
|
||
|
|
||
|
function shortcutHandler(event) {
|
||
|
if (!event.ctrlKey || (window.navigator.platform === 'MacIntel' && !event.metaKey) || event.shiftKey || event.altKey) return;
|
||
|
const textarea = event.target,
|
||
|
key = event.key.toLowerCase();
|
||
|
|
||
|
for (const id in textileSyntax) {
|
||
|
if (key === textileSyntax[id].options.shortcutKey) {
|
||
|
textileSyntax[id].action(textarea, textileSyntax[id].options);
|
||
|
event.preventDefault();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function setupToolbar() {
|
||
|
$$('.communication__toolbar').forEach(toolbar => {
|
||
|
toolbar.addEventListener('click', clickHandler);
|
||
|
});
|
||
|
$$('.js-toolbar-input').forEach(textarea => {
|
||
|
textarea.addEventListener('keydown', shortcutHandler);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
export { setupToolbar };
|