mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
initial implementation of markdown-based text editor
This commit is contained in:
parent
5bfcf56728
commit
71fa95e462
37 changed files with 511 additions and 468 deletions
|
@ -215,7 +215,7 @@ hr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//textile
|
// Text Editor
|
||||||
blockquote {
|
blockquote {
|
||||||
margin: 1em 2em;
|
margin: 1em 2em;
|
||||||
border: 1px dotted $foreground_color;
|
border: 1px dotted $foreground_color;
|
||||||
|
@ -298,7 +298,7 @@ blockquote blockquote blockquote blockquote blockquote blockquote {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textile-syntax-reference {
|
.editor-syntax-reference {
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -222,3 +222,50 @@ a.block__header--single-item, .block__header a {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block__content--top-border {
|
||||||
|
border-top: $border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block__column__header {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: $block_header_sub_height;
|
||||||
|
margin-bottom: $header_field_spacing;
|
||||||
|
border-bottom: 1px solid;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $min_px_width_for_desktop_layout) {
|
||||||
|
.block--split {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block__column--full, .block__column--half {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block__column--full {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block__column--half {
|
||||||
|
flex-basis: 50%;
|
||||||
|
max-width: 50%;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
padding-right: $block-spacing;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
padding-left: $block-spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex__grow {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -74,6 +74,10 @@ form p {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input--resize-vertical {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
margin: 0.2em 0 0 0.4em;
|
margin: 0.2em 0 0 0.4em;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -51,7 +51,7 @@ span.communication__sender__stats,
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.communication-edit__tab {
|
.communication-edit__wrap {
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ span.communication__sender__stats,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hyphenate-breaks{
|
.hyphenate-breaks {
|
||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
257
assets/js/markdowntoolbar.js
Normal file
257
assets/js/markdowntoolbar.js
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
/**
|
||||||
|
* Markdown toolbar
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { $, $$, showEl } from './utils/dom';
|
||||||
|
|
||||||
|
const markdownSyntax = {
|
||||||
|
bold: {
|
||||||
|
action: wrapSelection,
|
||||||
|
options: { prefix: '**', shortcutKey: 'b' }
|
||||||
|
},
|
||||||
|
italics: {
|
||||||
|
action: wrapSelection,
|
||||||
|
options: { prefix: '*', shortcutKey: 'i' }
|
||||||
|
},
|
||||||
|
under: {
|
||||||
|
action: wrapSelection,
|
||||||
|
options: { prefix: '__', shortcutKey: 'u' }
|
||||||
|
},
|
||||||
|
spoiler: {
|
||||||
|
action: wrapSelection,
|
||||||
|
options: { prefix: '||', shortcutKey: 's' }
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
action: wrapSelection,
|
||||||
|
options: { prefix: '`', shortcutKey: 'e' }
|
||||||
|
},
|
||||||
|
strike: {
|
||||||
|
action: wrapSelection,
|
||||||
|
options: { prefix: '~~' }
|
||||||
|
},
|
||||||
|
superscript: {
|
||||||
|
action: wrapSelection,
|
||||||
|
options: { prefix: '^' }
|
||||||
|
},
|
||||||
|
subscript: {
|
||||||
|
action: wrapSelection,
|
||||||
|
options: { prefix: '%' }
|
||||||
|
},
|
||||||
|
quote: {
|
||||||
|
action: wrapLines,
|
||||||
|
options: { prefix: '> ' }
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
action: insertLink,
|
||||||
|
options: { shortcutKey: 'l' }
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
action: insertLink,
|
||||||
|
options: { image: true, shortcutKey: 'k' }
|
||||||
|
},
|
||||||
|
noParse: {
|
||||||
|
action: escapeSelection,
|
||||||
|
options: { escapeChar: '\\' }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSelections(textarea, linesOnly = false) {
|
||||||
|
let { selectionStart, selectionEnd } = textarea,
|
||||||
|
selection = textarea.value.substring(selectionStart, selectionEnd),
|
||||||
|
leadingSpace = '',
|
||||||
|
trailingSpace = '',
|
||||||
|
caret;
|
||||||
|
|
||||||
|
if (linesOnly) {
|
||||||
|
let startNewlineIndex = 0,
|
||||||
|
endNewlineIndex = textarea.value.length,
|
||||||
|
explorer = /\n/g;
|
||||||
|
while (explorer.exec(textarea.value)) {
|
||||||
|
const { lastIndex } = explorer;
|
||||||
|
if (lastIndex < selectionStart) {
|
||||||
|
startNewlineIndex = lastIndex + 1;
|
||||||
|
} else if (lastIndex > selectionEnd) {
|
||||||
|
endNewlineIndex = lastIndex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionStart = startNewlineIndex;
|
||||||
|
selectionEnd = endNewlineIndex;
|
||||||
|
selection = textarea.value.substring(selectionStart, selectionEnd);
|
||||||
|
} else {
|
||||||
|
// 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, selectionStart) + leadingSpace,
|
||||||
|
afterSelection: trailingSpace + textarea.value.substring(selectionEnd),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSurroundingTwoLines(beforeText, afterText) {
|
||||||
|
// Selection typically includes the new line right before it
|
||||||
|
// therefore you need to include two lines before and after
|
||||||
|
return {
|
||||||
|
twoLinesBefore: beforeText.split('\n').slice(-2).join('\n'),
|
||||||
|
twoLinesAfter: afterText.split('\n').slice(0, 2).join('\n'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformSelection(textarea, transformer, eachLine) {
|
||||||
|
const { selectedText, beforeSelection, afterSelection } = getSelections(textarea, eachLine),
|
||||||
|
// For long comments, record scrollbar position to restore it later
|
||||||
|
{ scrollTop } = textarea;
|
||||||
|
|
||||||
|
const { newText, caretOffset } = transformer(selectedText, beforeSelection, afterSelection);
|
||||||
|
|
||||||
|
textarea.value = beforeSelection + newText + afterSelection;
|
||||||
|
|
||||||
|
const newSelectionStart = caretOffset >= 1
|
||||||
|
? beforeSelection.length + caretOffset
|
||||||
|
: textarea.value.length - afterSelection.length - caretOffset;
|
||||||
|
|
||||||
|
textarea.selectionStart = newSelectionStart;
|
||||||
|
textarea.selectionEnd = newSelectionStart;
|
||||||
|
textarea.scrollTop = scrollTop;
|
||||||
|
textarea.dispatchEvent(new Event('keydown'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertLink(textarea, options) {
|
||||||
|
let hyperlink = window.prompt(options.image ? 'Image link:' : 'Link:');
|
||||||
|
if (!hyperlink || hyperlink === '') return;
|
||||||
|
|
||||||
|
// Change on-site link to use relative url
|
||||||
|
if (!options.image && hyperlink.startsWith(window.location.origin)) {
|
||||||
|
hyperlink = hyperlink.substring(window.location.origin.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = options.image ? '![' : '[',
|
||||||
|
suffix = '](' + escapeHyperlink(hyperlink) + ')';
|
||||||
|
|
||||||
|
wrapSelection(textarea, { prefix, suffix });
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapSelection(textarea, options) {
|
||||||
|
transformSelection(textarea, selectedText => {
|
||||||
|
const { text = selectedText, prefix = '', suffix = options.prefix } = options,
|
||||||
|
emptyText = text === '';
|
||||||
|
let newText = text;
|
||||||
|
|
||||||
|
if (!emptyText) {
|
||||||
|
newText = text.replace(/(\n{2,})/g, match => {
|
||||||
|
return suffix + match + prefix;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
newText: prefix + newText + suffix,
|
||||||
|
caretOffset: emptyText ? prefix.length : -suffix.length,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapLines(textarea, options) {
|
||||||
|
transformSelection(textarea, (selectedText, before, after) => {
|
||||||
|
const { text = selectedText, prefix = '', suffix = '' } = options,
|
||||||
|
{ twoLinesBefore, twoLinesAfter } = getSurroundingTwoLines(before, after),
|
||||||
|
emptyText = text === '';
|
||||||
|
let newText = prefix;
|
||||||
|
|
||||||
|
if (!emptyText) {
|
||||||
|
newText = text.split(/\n/g).map(line => prefix + (line.trim()) + suffix).join('\n');
|
||||||
|
} else {
|
||||||
|
newText += suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add blank lines before/after if surrounding line are not empty
|
||||||
|
if (isNotBlank(twoLinesBefore)) newText = '\n' + newText;
|
||||||
|
if (isNotBlank(twoLinesAfter)) newText += '\n';
|
||||||
|
|
||||||
|
return { newText, caretOffset: newText.length - suffix.length };
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeSelection(textarea, options) {
|
||||||
|
transformSelection(textarea, selectedText => {
|
||||||
|
const { text = selectedText } = options,
|
||||||
|
emptyText = text === '';
|
||||||
|
|
||||||
|
if (emptyText) return;
|
||||||
|
|
||||||
|
let newText = text.replace(/([\[\]()*_`\\~<>^])/g, '\\$1').replace(/\|\|/g, '\\|\\|');
|
||||||
|
|
||||||
|
return {
|
||||||
|
newText: newText,
|
||||||
|
caretOffset: newText.length,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHyperlink(url) {
|
||||||
|
return typeof url === 'string' ? url.replace(/([()])/g, '\\$1') : url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNotBlank(string) {
|
||||||
|
return /\S/.test(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
markdownSyntax[id].action(textarea, markdownSyntax[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 markdownSyntax) {
|
||||||
|
if (key === markdownSyntax[id].options.shortcutKey) {
|
||||||
|
markdownSyntax[id].action(textarea, markdownSyntax[id].options);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupToolbar() {
|
||||||
|
$$('.communication__toolbar').forEach(toolbar => {
|
||||||
|
toolbar.addEventListener('click', clickHandler);
|
||||||
|
});
|
||||||
|
$$('.js-toolbar-input').forEach(textarea => {
|
||||||
|
textarea.addEventListener('keydown', shortcutHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transform non-JS basic editor to two-column layout with preview
|
||||||
|
$$('.js-preview-input-wrapper').forEach(wrapper => {
|
||||||
|
wrapper.classList.remove('block__column--full');
|
||||||
|
wrapper.classList.add('block__column--half');
|
||||||
|
});
|
||||||
|
$$('.js-preview-output-wrapper').forEach(wrapper => {
|
||||||
|
showEl(wrapper)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { setupToolbar };
|
|
@ -1,14 +1,16 @@
|
||||||
/**
|
/**
|
||||||
* Textile previews (posts, comments, messages)
|
* Markdown previews (posts, comments, messages)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { fetchJson } from './utils/requests';
|
import { fetchJson } from './utils/requests';
|
||||||
import { filterNode } from './imagesclientside';
|
import { filterNode } from './imagesclientside';
|
||||||
|
import { debounce } from './utils/events.js';
|
||||||
|
import { hideEl, showEl } from './utils/dom.js';
|
||||||
|
|
||||||
function handleError(response) {
|
function handleError(response) {
|
||||||
const errorMessage = '<div>Preview failed to load!</div>';
|
const errorMessage = '<div>Preview failed to load!</div>';
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok){
|
||||||
return errorMessage;
|
return errorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +24,7 @@ function commentReply(user, url, textarea, quote) {
|
||||||
if (newval && /\n$/.test(newval)) newval += '\n';
|
if (newval && /\n$/.test(newval)) newval += '\n';
|
||||||
newval += `${text}\n`;
|
newval += `${text}\n`;
|
||||||
|
|
||||||
if (quote) {
|
if (quote){
|
||||||
newval += `[bq="${user.replace('"', '\'')}"] ${quote} [/bq]\n`;
|
newval += `[bq="${user.replace('"', '\'')}"] ${quote} [/bq]\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,47 +37,99 @@ function commentReply(user, url, textarea, quote) {
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPreview(body, anonymous, previewTab, isImage = false) {
|
/**
|
||||||
|
* Stores the abort controller for the current preview request
|
||||||
|
* @type {null|AbortController}
|
||||||
|
*/
|
||||||
|
let previewAbortController = null;
|
||||||
|
|
||||||
|
function getPreview(body, anonymous, previewLoading, previewContent) {
|
||||||
let path = '/posts/preview';
|
let path = '/posts/preview';
|
||||||
|
|
||||||
fetchJson('POST', path, { body, anonymous })
|
if (typeof body !== 'string') return;
|
||||||
|
|
||||||
|
const trimmedBody = body.trim();
|
||||||
|
if (trimmedBody.length < 1){
|
||||||
|
previewContent.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showEl(previewLoading);
|
||||||
|
|
||||||
|
// Abort previous requests if it exists
|
||||||
|
if (previewAbortController) previewAbortController.abort();
|
||||||
|
previewAbortController = new AbortController();
|
||||||
|
|
||||||
|
fetchJson('POST', path, { body, anonymous }, previewAbortController.signal)
|
||||||
.then(handleError)
|
.then(handleError)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
previewTab.innerHTML = data;
|
previewContent.innerHTML = data;
|
||||||
filterNode(previewTab);
|
filterNode(previewContent);
|
||||||
|
showEl(previewContent);
|
||||||
|
hideEl(previewLoading);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
previewAbortController = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resizes the event target <textarea> to match the size of its contained text, between set
|
||||||
|
* minimum and maximum height values. Former comes from CSS, latter is hard coded below.
|
||||||
|
* @template {{ target: HTMLTextAreaElement }} E
|
||||||
|
* @param {E} e
|
||||||
|
*/
|
||||||
|
function resizeTextarea(e) {
|
||||||
|
// Reset inline height for fresh calculations
|
||||||
|
e.target.style.height = '';
|
||||||
|
const { borderTopWidth, borderBottomWidth, height } = window.getComputedStyle(e.target);
|
||||||
|
// Add scrollHeight and borders (because border-box) to get the target size that avoids scrollbars
|
||||||
|
const contentHeight = e.target.scrollHeight + parseFloat(borderTopWidth) + parseFloat(borderBottomWidth);
|
||||||
|
// Get the original default height provided by page styles
|
||||||
|
const regularHeight = parseFloat(height);
|
||||||
|
// Limit textarea's size to between the original height and 1000px
|
||||||
|
const newHeight = Math.max(regularHeight, Math.min(1000, contentHeight));
|
||||||
|
e.target.style.height = `${newHeight}px`;
|
||||||
|
}
|
||||||
|
|
||||||
function setupPreviews() {
|
function setupPreviews() {
|
||||||
let textarea = document.querySelector('.js-preview-input');
|
let textarea = document.querySelector('.js-preview-input');
|
||||||
let imageDesc = false;
|
|
||||||
|
|
||||||
if (!textarea) {
|
if (!textarea){
|
||||||
textarea = document.querySelector('.js-preview-description');
|
textarea = document.querySelector('.js-preview-description');
|
||||||
imageDesc = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const previewButton = document.querySelector('a[data-click-tab="preview"]');
|
const previewLoading = document.querySelector('.communication-preview__loading');
|
||||||
const previewTab = document.querySelector('.block__tab[data-tab="preview"]');
|
const previewContent = document.querySelector('.communication-preview__content');
|
||||||
const previewAnon = document.querySelector('.preview-anonymous') || false;
|
const previewAnon = document.querySelector('.js-preview-anonymous') || false;
|
||||||
|
|
||||||
if (!textarea || !previewButton) {
|
if (!textarea || !previewContent){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
previewButton.addEventListener('click', () => {
|
const updatePreview = () => {
|
||||||
if (previewTab.previewedText === textarea.value) return;
|
getPreview(textarea.value, previewAnon && previewAnon.checked, previewLoading, previewContent);
|
||||||
previewTab.previewedText = textarea.value;
|
};
|
||||||
|
|
||||||
getPreview(textarea.value, Boolean(previewAnon.checked), previewTab, imageDesc);
|
const debouncedUpdater = debounce(500, () => {
|
||||||
|
if (previewContent.previewedText === textarea.value) return;
|
||||||
|
previewContent.previewedText = textarea.value;
|
||||||
|
|
||||||
|
updatePreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
previewAnon && previewAnon.addEventListener('click', () => {
|
textarea.addEventListener('keydown', debouncedUpdater);
|
||||||
getPreview(textarea.value, Boolean(previewAnon.checked), previewTab, imageDesc);
|
textarea.addEventListener('focus', debouncedUpdater);
|
||||||
});
|
textarea.addEventListener('change', resizeTextarea);
|
||||||
|
textarea.addEventListener('keyup', resizeTextarea);
|
||||||
|
|
||||||
|
// Fire handler if textarea contains text on page load (e.g. editing)
|
||||||
|
if (textarea.value) textarea.dispatchEvent(new Event('keydown'));
|
||||||
|
|
||||||
|
previewAnon && previewAnon.addEventListener('click', updatePreview);
|
||||||
|
|
||||||
document.addEventListener('click', event => {
|
document.addEventListener('click', event => {
|
||||||
if (event.target && event.target.closest('.post-reply')) {
|
if (event.target && event.target.closest('.post-reply')){
|
||||||
const link = event.target.closest('.post-reply');
|
const link = event.target.closest('.post-reply');
|
||||||
commentReply(link.dataset.author, link.getAttribute('href'), textarea, link.dataset.post);
|
commentReply(link.dataset.author, link.getAttribute('href'), textarea, link.dataset.post);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -83,4 +137,4 @@ function setupPreviews() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { setupPreviews };
|
export { setupPreviews };
|
||||||
|
|
|
@ -1,172 +0,0 @@
|
||||||
/**
|
|
||||||
* Textile toolbar
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { $, $$ } from './utils/dom';
|
|
||||||
|
|
||||||
const textileSyntax = {
|
|
||||||
bold: {
|
|
||||||
action: wrapSelection,
|
|
||||||
options: { prefix: '*', suffix: '*', shortcutKey: 'b', type: 'inline' }
|
|
||||||
},
|
|
||||||
italics: {
|
|
||||||
action: wrapSelection,
|
|
||||||
options: { prefix: '_', suffix: '_', shortcutKey: 'i', type: 'inline' }
|
|
||||||
},
|
|
||||||
under: {
|
|
||||||
action: wrapSelection,
|
|
||||||
options: { prefix: '+', suffix: '+', shortcutKey: 'u', type: 'inline' }
|
|
||||||
},
|
|
||||||
spoiler: {
|
|
||||||
action: wrapSelection,
|
|
||||||
options: { prefix: '[spoiler]', suffix: '[/spoiler]', shortcutKey: 's' }
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
action: wrapSelection,
|
|
||||||
options: { prefix: '@', suffix: '@', shortcutKey: 'e', type: 'inline' }
|
|
||||||
},
|
|
||||||
strike: {
|
|
||||||
action: wrapSelection,
|
|
||||||
options: { prefix: '-', suffix: '-', type: 'inline' }
|
|
||||||
},
|
|
||||||
superscript: {
|
|
||||||
action: wrapSelection,
|
|
||||||
options: { prefix: '^', suffix: '^', type: 'inline' }
|
|
||||||
},
|
|
||||||
subscript: {
|
|
||||||
action: wrapSelection,
|
|
||||||
options: { prefix: '~', suffix: '~', type: 'inline' }
|
|
||||||
},
|
|
||||||
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 = '', type } = options,
|
|
||||||
// For long comments, record scrollbar position to restore it later
|
|
||||||
scrollTop = textarea.scrollTop,
|
|
||||||
emptyText = text === '';
|
|
||||||
|
|
||||||
const newText = text;
|
|
||||||
|
|
||||||
if (type === 'inline' && newText.includes('\n')) {
|
|
||||||
textarea.value = `${beforeSelection}[${prefix}${newText}${suffix}]${afterSelection}`;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
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 };
|
|
|
@ -22,3 +22,22 @@ export function delegate(node, event, selectors) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the provided `func` if it hasn't been called for at least `time` ms
|
||||||
|
* @template {(...any[]) => any} T
|
||||||
|
* @param {number} time
|
||||||
|
* @param {T} func
|
||||||
|
* @return {T}
|
||||||
|
*/
|
||||||
|
export function debounce(time, func) {
|
||||||
|
let timerId = null;
|
||||||
|
|
||||||
|
return function(...args) {
|
||||||
|
// Cancels the setTimeout method execution
|
||||||
|
timerId && clearTimeout(timerId);
|
||||||
|
|
||||||
|
// Executes the func after delay time.
|
||||||
|
timerId = setTimeout(() => func(...args), time);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* Request Utils
|
* Request Utils
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function fetchJson(verb, endpoint, body) {
|
function fetchJson(verb, endpoint, body, signal) {
|
||||||
const data = {
|
const data = {
|
||||||
method: verb,
|
method: verb,
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
|
@ -11,6 +11,7 @@ function fetchJson(verb, endpoint, body) {
|
||||||
'x-csrf-token': window.booru.csrfToken,
|
'x-csrf-token': window.booru.csrfToken,
|
||||||
'x-requested-with': 'xmlhttprequest'
|
'x-requested-with': 'xmlhttprequest'
|
||||||
},
|
},
|
||||||
|
signal,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (body) {
|
if (body) {
|
||||||
|
|
|
@ -31,7 +31,7 @@ import { setupTagEvents } from './tagsmisc';
|
||||||
import { setupTimestamps } from './timeago';
|
import { setupTimestamps } from './timeago';
|
||||||
import { setupImageUpload } from './upload';
|
import { setupImageUpload } from './upload';
|
||||||
import { setupSearch } from './search';
|
import { setupSearch } from './search';
|
||||||
import { setupToolbar } from './textiletoolbar';
|
import { setupToolbar } from './markdowntoolbar.js';
|
||||||
import { hideStaffTools } from './staffhider';
|
import { hideStaffTools } from './staffhider';
|
||||||
import { pollOptionCreator } from './poll';
|
import { pollOptionCreator } from './poll';
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ article.block.communication id="comment_#{@comment.id}"
|
||||||
.flex__fixed.spacing-right
|
.flex__fixed.spacing-right
|
||||||
.post-image-container
|
.post-image-container
|
||||||
= render PhilomenaWeb.ImageView, "_image_container.html", image: @comment.image, size: :thumb_tiny, conn: @conn
|
= render PhilomenaWeb.ImageView, "_image_container.html", image: @comment.image, size: :thumb_tiny, conn: @conn
|
||||||
|
|
||||||
.flex__grow.communication__body
|
.flex__grow.communication__body
|
||||||
span.communication__body__sender-name = render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @comment, awards: true, conn: @conn
|
span.communication__body__sender-name = render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @comment, awards: true, conn: @conn
|
||||||
br
|
br
|
||||||
|
|
|
@ -1,24 +1,7 @@
|
||||||
= form_for @changeset, Routes.conversation_message_path(@conn, :create, @conversation), fn f ->
|
= form_for @changeset, Routes.conversation_message_path(@conn, :create, @conversation), fn f ->
|
||||||
.block
|
.block
|
||||||
.block__header.block__header--js-tabbed
|
.communication-edit__wrap
|
||||||
a.selected href="#" data-click-tab="write"
|
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, action_icon: "pencil-alt", action_text: "Reply"
|
||||||
i.fa.fa-pencil-alt>
|
|
||||||
| Reply
|
|
||||||
|
|
||||||
a href="#" data-click-tab="preview"
|
|
||||||
i.fa.fa-eye>
|
|
||||||
| Preview
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.selected data-tab="write"
|
|
||||||
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
|
|
||||||
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
|
|
||||||
|
|
||||||
.field
|
|
||||||
= textarea f, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Your message", required: true
|
|
||||||
= error_tag f, :body
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.hidden data-tab="preview"
|
|
||||||
| [Loading preview...]
|
|
||||||
|
|
||||||
.block__content.communication-edit__actions
|
.block__content.communication-edit__actions
|
||||||
= submit "Send", class: "button", autocomplete: "off", data: [disable_with: "Sending..."]
|
= submit "Send", class: "button", autocomplete: "off", data: [disable_with: raw("Sending…")]
|
||||||
|
|
|
@ -20,25 +20,8 @@ h1 New Conversation
|
||||||
= error_tag f, :title
|
= error_tag f, :title
|
||||||
|
|
||||||
= inputs_for f, :messages, fn fm ->
|
= inputs_for f, :messages, fn fm ->
|
||||||
.block
|
div
|
||||||
.block__header.block__header--js-tabbed
|
= render PhilomenaWeb.MarkdownView, "_input.html", changeset: @changeset, conn: @conn, f: fm, action_icon: "pencil-alt", action_text: "Compose"
|
||||||
a.selected href="#" data-click-tab="write"
|
|
||||||
i.fa.fa-pencil-alt>
|
|
||||||
| Reply
|
|
||||||
|
|
||||||
a href="#" data-click-tab="preview"
|
|
||||||
i.fa.fa-eye>
|
|
||||||
| Preview
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.selected data-tab="write"
|
|
||||||
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
|
|
||||||
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
|
|
||||||
|
|
||||||
= textarea fm, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Your message", required: true
|
|
||||||
= error_tag fm, :body
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.hidden data-tab="preview"
|
|
||||||
| [Loading preview...]
|
|
||||||
|
|
||||||
.block__content.communication-edit__actions
|
.block__content.communication-edit__actions
|
||||||
= submit "Send", class: "button", autocomplete: "off", data: [disable_with: "Sending..."]
|
= submit "Send", class: "button", autocomplete: "off", data: [disable_with: "Sending..."]
|
||||||
|
|
|
@ -6,29 +6,10 @@
|
||||||
p Oops, something went wrong! Please check the errors below.
|
p Oops, something went wrong! Please check the errors below.
|
||||||
|
|
||||||
.block
|
.block
|
||||||
.block__header.block__header--js-tabbed
|
div
|
||||||
a.selected href="#" data-click-tab="write"
|
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, placeholder: "Please read the site rules before posting and use ||spoilers|| for above-rating stuff."
|
||||||
i.fas.fa-edit>
|
|
||||||
' Edit
|
|
||||||
|
|
||||||
a href="#" data-click-tab="preview"
|
|
||||||
i.fa.fa-eye>
|
|
||||||
' Preview
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.selected data-tab="write"
|
|
||||||
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
|
|
||||||
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
|
|
||||||
|
|
||||||
.field
|
|
||||||
= textarea f, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Please read the site rules before posting and use [spoiler][/spoiler] for above-rating stuff.", required: true
|
|
||||||
= error_tag f, :body
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.hidden data-tab="preview"
|
|
||||||
' [Loading preview...]
|
|
||||||
|
|
||||||
.block__content.communication-edit__actions
|
.block__content.communication-edit__actions
|
||||||
=> submit "Post", class: "button", data: [disable_with: raw("Posting…")]
|
=> submit "Post", class: "button", data: [disable_with: raw("Posting…")]
|
||||||
|
|
||||||
= if @conn.assigns.current_user do
|
= render PhilomenaWeb.MarkdownView, "_anon_checkbox.html", conn: @conn, f: f
|
||||||
= checkbox f, :anonymous, value: anonymous_by_default?(@conn)
|
|
||||||
= label f, :anonymous, "Anonymous"
|
|
||||||
|
|
|
@ -4,29 +4,12 @@
|
||||||
p Oops, something went wrong! Please check the errors below.
|
p Oops, something went wrong! Please check the errors below.
|
||||||
|
|
||||||
.block
|
.block
|
||||||
.block__header.block__header--js-tabbed
|
.communication-edit__wrap
|
||||||
a.selected href="#" data-click-tab="write"
|
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, placeholder: "Please read the site rules before posting and use ||spoilers|| for above-rating stuff."
|
||||||
i.fas.fa-edit>
|
|
||||||
' Edit
|
|
||||||
|
|
||||||
a href="#" data-click-tab="preview"
|
.block__content.field
|
||||||
i.fa.fa-eye>
|
= text_input f, :edit_reason, class: "input input--wide", placeholder: "Reason for edit"
|
||||||
' Preview
|
= error_tag f, :edit_reason
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.selected data-tab="write"
|
|
||||||
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
|
|
||||||
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
|
|
||||||
|
|
||||||
.field
|
|
||||||
= textarea f, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Please read the site rules before posting and use [spoiler][/spoiler] for above-rating stuff.", required: true
|
|
||||||
= error_tag f, :body
|
|
||||||
|
|
||||||
.field
|
|
||||||
= text_input f, :edit_reason, class: "input input--wide", placeholder: "Reason for edit"
|
|
||||||
= error_tag f, :edit_reason
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.hidden data-tab="preview"
|
|
||||||
' [Loading preview...]
|
|
||||||
|
|
||||||
.block__content.communication-edit__actions
|
.block__content.communication-edit__actions
|
||||||
=> submit "Edit", class: "button", data: [disable_with: raw("Posting…")]
|
=> submit "Edit", class: "button", data: [disable_with: raw("Posting…")]
|
||||||
|
|
|
@ -43,4 +43,4 @@ h1
|
||||||
' Edited
|
' Edited
|
||||||
=> pretty_time(version.created_at)
|
=> pretty_time(version.created_at)
|
||||||
' by
|
' by
|
||||||
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: version, conn: @conn
|
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: version, conn: @conn
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
.alert.alert-danger
|
.alert.alert-danger
|
||||||
p Oops, something went wrong! Please check the errors below.
|
p Oops, something went wrong! Please check the errors below.
|
||||||
|
|
||||||
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
|
= render PhilomenaWeb.MarkdownView, "_help.html", conn: @conn
|
||||||
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
|
= render PhilomenaWeb.MarkdownView, "_toolbar.html", conn: @conn
|
||||||
|
|
||||||
.field
|
.field
|
||||||
= textarea f, :description, id: "description", class: "input input--wide js-toolbar-input", placeholder: "Describe this image in plain words - this should generally be info about the image that doesn't belong in the tags or source."
|
= textarea f, :description, id: "description", class: "input input--wide js-toolbar-input", placeholder: "Describe this image in plain words - this should generally be info about the image that doesn't belong in the tags or source."
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
.field
|
.field
|
||||||
= label f, :source_url, "The page you found this image on"
|
= label f, :source_url, "The page you found this image on"
|
||||||
= url_input f, :source_url, class: "input input--wide js-image-input", placeholder: "Source URL"
|
= url_input f, :source_url, class: "input input--wide js-image-input", placeholder: "Source URL"
|
||||||
|
|
||||||
.field
|
.field
|
||||||
label for="image[tag_input]"
|
label for="image[tag_input]"
|
||||||
' Describe with
|
' Describe with
|
||||||
|
@ -64,24 +64,12 @@
|
||||||
|
|
||||||
.field
|
.field
|
||||||
.block
|
.block
|
||||||
.block__header.block__header--js-tabbed
|
.communication-edit__wrap
|
||||||
= link "Description", to: "#", class: "selected", data: [click_tab: "write"]
|
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, action_icon: "pencil-alt", action_text: "Description", placeholder: "Describe this image in plain words - this should generally be info about the image that doesn't belong in the tags or source."
|
||||||
= link "Preview", to: "#", data: [click_tab: "preview"]
|
|
||||||
|
|
||||||
.block__tab.selected data-tab="write"
|
= render PhilomenaWeb.MarkdownView, "_anon_checkbox.html", conn: @conn, f: f, label: "Post anonymously"
|
||||||
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
|
|
||||||
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
|
|
||||||
|
|
||||||
= textarea f, :description, class: "input input--wide input--text js-preview-description js-image-input js-toolbar-input", placeholder: "Describe this image in plain words - this should generally be info about the image that doesn't belong in the tags or source."
|
|
||||||
.block__tab.hidden data-tab="preview"
|
|
||||||
| Loading preview...
|
|
||||||
|
|
||||||
= if @conn.assigns.current_user do
|
|
||||||
.field
|
|
||||||
= label f, :anonymous, "Post anonymously"
|
|
||||||
= checkbox f, :anonymous, class: "checkbox", value: anonymous_by_default?(@conn)
|
|
||||||
|
|
||||||
= render PhilomenaWeb.CaptchaView, "_captcha.html", name: "image", conn: @conn
|
= render PhilomenaWeb.CaptchaView, "_captcha.html", name: "image", conn: @conn
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
= submit "Upload", class: "button", autocomplete: "off", data: [disable_with: "Please wait..."]
|
= submit "Upload", class: "button input--separate-top", autocomplete: "off", data: [disable_with: "Please wait..."]
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
- f = assigns[:f]
|
||||||
|
- label_lext = assigns[:label] || "Anonymous"
|
||||||
|
= if @conn.assigns.current_user do
|
||||||
|
=> checkbox f, :anonymous, value: anonymous_by_default?(@conn), class: "js-preview-anonymous"
|
||||||
|
= label f, :anonymous, label_lext
|
|
@ -1,15 +1,13 @@
|
||||||
.textile-syntax-reference
|
.editor-syntax-reference
|
||||||
strong<> Syntax quick reference:
|
strong<> Syntax quick reference:
|
||||||
|
|
||||||
span
|
span
|
||||||
strong>
|
strong> **bold**
|
||||||
' *bold*
|
em> *italic*
|
||||||
em> _italic_
|
span.spoiler> ||hide text||
|
||||||
span.spoiler>
|
code> `code`
|
||||||
| [spoiler]hide text[/spoiler]
|
ins> __underline__
|
||||||
code> @code@
|
del> ~~strike~~
|
||||||
ins> +underline+
|
|
||||||
del> -strike-
|
|
||||||
sup> ^sup^
|
sup> ^sup^
|
||||||
sub ~sub~
|
sub ~sub~
|
||||||
|
|
||||||
|
@ -17,11 +15,11 @@
|
||||||
' …
|
' …
|
||||||
|
|
||||||
p.hidden.textile_help
|
p.hidden.textile_help
|
||||||
' [==stuff you don't want textile to parse==]
|
' Escape formatting using \ e.g. \* for a literal star character
|
||||||
br
|
br
|
||||||
|
|
||||||
ins> Links:
|
ins> Links:
|
||||||
' "On-site link":/some-link, "External link":http://some-link
|
' [On-site link](/some-link), [External link](http://some-link)
|
||||||
br
|
br
|
||||||
|
|
||||||
ins> Images:
|
ins> Images:
|
||||||
|
@ -29,7 +27,7 @@
|
||||||
br
|
br
|
||||||
|
|
||||||
ins> External images:
|
ins> External images:
|
||||||
' !http://some-image!, !http://some-clickable-image!:http://some-link
|
' ![alt text](http://some-image), [![alt text](http://some-clickable-image)](http://some-link)
|
||||||
br
|
br
|
||||||
|
|
||||||
strong> Remember to use embeds (>>) for booru images as these let users filter content they don't want to see
|
strong> Remember to use embeds (>>) for booru images as these let users filter content they don't want to see
|
27
lib/philomena_web/templates/markdown/_input.html.slime
Normal file
27
lib/philomena_web/templates/markdown/_input.html.slime
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
- form = assigns[:f]
|
||||||
|
- action_text = assigns[:action_text] || 'Edit'
|
||||||
|
- action_icon = assigns[:action_icon] || 'edit'
|
||||||
|
- field_name = assigns[:name] || :body
|
||||||
|
- field_placeholder = assigns[:placeholder] || "Your message"
|
||||||
|
.block__content.block--split
|
||||||
|
.block__column--full.js-preview-input-wrapper
|
||||||
|
.block__column__header
|
||||||
|
i.fa> class="fa-#{action_icon}"
|
||||||
|
= action_text
|
||||||
|
|
||||||
|
= render PhilomenaWeb.MarkdownView, "_help.html", conn: @conn
|
||||||
|
= render PhilomenaWeb.MarkdownView, "_toolbar.html", conn: @conn
|
||||||
|
|
||||||
|
.field
|
||||||
|
= textarea form, field_name, class: "input input--wide input--text input--resize-vertical js-toolbar-input js-preview-input", placeholder: field_placeholder, required: true
|
||||||
|
= error_tag form, field_name
|
||||||
|
|
||||||
|
.block__column--half.hidden.communication-preview.js-preview-output-wrapper
|
||||||
|
.block__column__header.flex.flex--spaced-out
|
||||||
|
span
|
||||||
|
i.fa.fa-eye>
|
||||||
|
' Preview
|
||||||
|
span.communication-preview__loading.hidden
|
||||||
|
i.fa.fa-spin.fa-fw.fa-cog> title=raw('Loading preview…')
|
||||||
|
|
||||||
|
.communication-preview__content
|
|
@ -31,4 +31,4 @@
|
||||||
i.fa.fa-image
|
i.fa.fa-image
|
||||||
button.communication__toolbar__button tabindex="-1" type="button" title="Text you want the parser to ignore" data-syntax-id="noParse"
|
button.communication__toolbar__button tabindex="-1" type="button" title="Text you want the parser to ignore" data-syntax-id="noParse"
|
||||||
span
|
span
|
||||||
| no parse
|
| escape
|
|
@ -2,9 +2,9 @@
|
||||||
.flex__fixed.spacing-right
|
.flex__fixed.spacing-right
|
||||||
= render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @post, conn: @conn
|
= render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @post, conn: @conn
|
||||||
|
|
||||||
.flex__grow.communication_body
|
.flex__grow.communication__body
|
||||||
span.communication__body__sender-name
|
span.communication__body__sender-name
|
||||||
= render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @post, conn: @conn, awards: true
|
= render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @post, conn: @conn, awards: true
|
||||||
|
|
||||||
.communication__body__text
|
.communication__body__text
|
||||||
== @body
|
== @body
|
||||||
|
|
|
@ -14,25 +14,8 @@ h1 Updating Profile Description
|
||||||
= error_tag f, :personal_title
|
= error_tag f, :personal_title
|
||||||
|
|
||||||
.block
|
.block
|
||||||
.block__header.block__header--js-tabbed
|
div
|
||||||
a.selected href="#" data-click-tab="write"
|
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, action_text: "About Me", placeholder: "Description (up to 10000 characters)", name: :description
|
||||||
i.fas.fa-edit>
|
|
||||||
' About Me
|
|
||||||
|
|
||||||
a href="#" data-click-tab="preview"
|
|
||||||
i.fa.fa-eye>
|
|
||||||
' Preview
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.selected data-tab="write"
|
|
||||||
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
|
|
||||||
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
|
|
||||||
|
|
||||||
.field
|
|
||||||
= textarea f, :description, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Description (up to 10000 characters)"
|
|
||||||
= error_tag f, :description
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.hidden data-tab="preview"
|
|
||||||
' [Loading preview...]
|
|
||||||
|
|
||||||
.block__content.communication-edit__actions
|
.block__content.communication-edit__actions
|
||||||
=> submit "Update", class: "button"
|
=> submit "Update", class: "button"
|
||||||
|
|
|
@ -5,25 +5,8 @@ h1 Updating Moderation Scratchpad
|
||||||
.alert.alert-danger
|
.alert.alert-danger
|
||||||
p Oops, something went wrong! Please check the errors below.
|
p Oops, something went wrong! Please check the errors below.
|
||||||
.block
|
.block
|
||||||
.block__header.block__header--js-tabbed
|
div
|
||||||
a.selected href="#" data-click-tab="write"
|
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, action_text: "Scratchpad", placeholder: "Scratchpad Contents", name: :scratchpad
|
||||||
i.fas.fa-edit>
|
|
||||||
' Scratchpad
|
|
||||||
|
|
||||||
a href="#" data-click-tab="preview"
|
|
||||||
i.fa.fa-eye>
|
|
||||||
' Preview
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.selected data-tab="write"
|
|
||||||
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
|
|
||||||
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
|
|
||||||
|
|
||||||
.field
|
|
||||||
= textarea f, :scratchpad, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Scratchpad Contents"
|
|
||||||
= error_tag f, :scratchpad
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.hidden data-tab="preview"
|
|
||||||
' [Loading preview...]
|
|
||||||
|
|
||||||
.block__content.communication-edit__actions
|
.block__content.communication-edit__actions
|
||||||
=> submit "Update", class: "button"
|
=> submit "Update", class: "button"
|
||||||
|
|
|
@ -45,24 +45,7 @@ p
|
||||||
.field
|
.field
|
||||||
= select f, :category, report_categories(), class: "input"
|
= select f, :category, report_categories(), class: "input"
|
||||||
.block
|
.block
|
||||||
.block__header.block__header--js-tabbed
|
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, placeholder: "Provide anything else we should know here.", name: :reason
|
||||||
a.selected href="#" data-click-tab="write"
|
|
||||||
i.fas.fa-edit>
|
|
||||||
' Edit
|
|
||||||
|
|
||||||
a href="#" data-click-tab="preview"
|
|
||||||
i.fa.fa-eye>
|
|
||||||
' Preview
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.selected data-tab="write"
|
|
||||||
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
|
|
||||||
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
|
|
||||||
|
|
||||||
.field
|
|
||||||
= textarea f, :reason, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Provide anything else we should know here."
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.hidden data-tab="preview"
|
|
||||||
' [Loading preview...]
|
|
||||||
|
|
||||||
= render PhilomenaWeb.CaptchaView, "_captcha.html", name: "report", conn: @conn
|
= render PhilomenaWeb.CaptchaView, "_captcha.html", name: "report", conn: @conn
|
||||||
|
|
||||||
|
|
|
@ -3,43 +3,26 @@
|
||||||
.alert.alert-danger
|
.alert.alert-danger
|
||||||
p Oops, something went wrong! Please check the errors below.
|
p Oops, something went wrong! Please check the errors below.
|
||||||
|
|
||||||
|
h1 Create a Topic
|
||||||
|
|
||||||
|
.field
|
||||||
|
= text_input f, :title, class: "input input--wide", placeholder: "Title"
|
||||||
|
= error_tag f, :title
|
||||||
|
= error_tag f, :slug
|
||||||
|
|
||||||
.block
|
.block
|
||||||
.block__header.block__header--js-tabbed
|
= inputs_for f, :posts, fn fp ->
|
||||||
a.selected href="#" data-click-tab="write"
|
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: fp, action_icon: "pencil-alt", action_text: "First Post", placeholder: "Please read the site rules before posting and use ||spoilers|| for NSFW stuff in SFW forums."
|
||||||
i.fas.fa-pencil-alt>
|
|
||||||
' Create a Topic
|
|
||||||
|
|
||||||
a href="#" data-click-tab="preview"
|
.block__content.communication-edit__wrap
|
||||||
i.fa.fa-eye>
|
= render PhilomenaWeb.MarkdownView, "_anon_checkbox.html", conn: @conn, f: f, label: "Post anonymously"
|
||||||
' Preview
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.selected data-tab="write"
|
|
||||||
.field
|
|
||||||
= text_input f, :title, class: "input input--wide", placeholder: "Title"
|
|
||||||
= error_tag f, :title
|
|
||||||
= error_tag f, :slug
|
|
||||||
|
|
||||||
= inputs_for f, :posts, fn fp ->
|
|
||||||
.field
|
|
||||||
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
|
|
||||||
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
|
|
||||||
= textarea fp, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Please read the site rules before posting and use [spoiler][/spoiler] for NSFW stuff in SFW forums.", required: true
|
|
||||||
= error_tag fp, :body
|
|
||||||
|
|
||||||
= if @conn.assigns.current_user do
|
|
||||||
.field
|
|
||||||
=> checkbox f, :anonymous, value: anonymous_by_default?(@conn)
|
|
||||||
= label f, :anonymous, "Post anonymously"
|
|
||||||
|
|
||||||
= inputs_for f, :poll, fn fp ->
|
= inputs_for f, :poll, fn fp ->
|
||||||
#add-poll
|
#add-poll.field
|
||||||
input.toggle-box id="add_poll" name="add_poll" type="checkbox"
|
input.toggle-box id="add_poll" name="add_poll" type="checkbox"
|
||||||
label for="add_poll" Add a poll
|
label for="add_poll" Add a poll
|
||||||
.toggle-box-container
|
.toggle-box-container
|
||||||
= render PhilomenaWeb.Topic.PollView, "_form.html", Map.put(assigns, :f, fp)
|
= render PhilomenaWeb.Topic.PollView, "_form.html", Map.put(assigns, :f, fp)
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.hidden data-tab="preview"
|
|
||||||
' [Loading preview...]
|
|
||||||
|
|
||||||
.block__content.communication-edit__actions
|
.block__content.communication-edit__actions
|
||||||
= submit "Post", class: "button", data: [disable_with: raw("Posting…")]
|
= submit "Post", class: "button", data: [disable_with: raw("Posting…")]
|
||||||
|
|
|
@ -4,29 +4,10 @@
|
||||||
p Oops, something went wrong! Please check the errors below.
|
p Oops, something went wrong! Please check the errors below.
|
||||||
|
|
||||||
.block
|
.block
|
||||||
.block__header.block__header--js-tabbed
|
div
|
||||||
a.selected href="#" data-click-tab="write"
|
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, placeholder: "Please read the site rules before posting and use ||spoilers|| for NSFW stuff in SFW forums."
|
||||||
i.fas.fa-edit>
|
|
||||||
' Edit
|
|
||||||
|
|
||||||
a href="#" data-click-tab="preview"
|
|
||||||
i.fa.fa-eye>
|
|
||||||
' Preview
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.selected data-tab="write"
|
|
||||||
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
|
|
||||||
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
|
|
||||||
|
|
||||||
.field
|
|
||||||
= textarea f, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Please read the site rules before posting and use [spoiler][/spoiler] for NSFW stuff in SFW forums.", required: true
|
|
||||||
= error_tag f, :body
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.hidden data-tab="preview"
|
|
||||||
' [Loading preview...]
|
|
||||||
|
|
||||||
.block__content.communication-edit__actions
|
.block__content.communication-edit__actions
|
||||||
=> submit "Post", class: "button", data: [disable_with: raw("Posting…")]
|
=> submit "Post", class: "button", data: [disable_with: raw("Posting…")]
|
||||||
|
|
||||||
= if @conn.assigns.current_user do
|
= render PhilomenaWeb.MarkdownView, "_anon_checkbox.html", conn: @conn, f: f
|
||||||
= checkbox f, :anonymous, value: anonymous_by_default?(@conn)
|
|
||||||
= label f, :anonymous, "Anonymous"
|
|
||||||
|
|
|
@ -4,29 +4,13 @@
|
||||||
p Oops, something went wrong! Please check the errors below.
|
p Oops, something went wrong! Please check the errors below.
|
||||||
|
|
||||||
.block
|
.block
|
||||||
.block__header.block__header--js-tabbed
|
.communication-edit__wrap
|
||||||
a.selected href="#" data-click-tab="write"
|
|
||||||
i.fas.fa-edit>
|
|
||||||
' Edit
|
|
||||||
|
|
||||||
a href="#" data-click-tab="preview"
|
|
||||||
i.fa.fa-eye>
|
|
||||||
' Preview
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.selected data-tab="write"
|
|
||||||
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
|
|
||||||
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
|
|
||||||
|
|
||||||
.field
|
.field
|
||||||
= textarea f, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Please read the site rules before posting and use [spoiler][/spoiler] for above-rating stuff.", required: true
|
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, placeholder: "Please read the site rules before posting and use ||spoilers|| for NSFW stuff in SFW forums."
|
||||||
= error_tag f, :body
|
|
||||||
|
|
||||||
.field
|
.block__content.field
|
||||||
= text_input f, :edit_reason, class: "input input--wide", placeholder: "Reason for edit"
|
= text_input f, :edit_reason, class: "input input--wide", placeholder: "Reason for edit"
|
||||||
= error_tag f, :edit_reason
|
= error_tag f, :edit_reason
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.hidden data-tab="preview"
|
|
||||||
' [Loading preview...]
|
|
||||||
|
|
||||||
.block__content.communication-edit__actions
|
.block__content.communication-edit__actions
|
||||||
=> submit "Edit", class: "button", data: [disable_with: raw("Posting…")]
|
=> submit "Edit", class: "button", data: [disable_with: raw("Posting…")]
|
||||||
|
|
|
@ -42,4 +42,4 @@ h1
|
||||||
' Edited
|
' Edited
|
||||||
=> pretty_time(version.created_at)
|
=> pretty_time(version.created_at)
|
||||||
' by
|
' by
|
||||||
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: version, conn: @conn
|
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: version, conn: @conn
|
||||||
|
|
|
@ -108,7 +108,7 @@ h1 = @topic.title
|
||||||
- true ->
|
- true ->
|
||||||
|
|
||||||
= if can?(@conn, :hide, @topic) do
|
= if can?(@conn, :hide, @topic) do
|
||||||
.block__content
|
.block__content.block__content--top-border
|
||||||
input.toggle-box id="administrator_tools" type="checkbox" checked=false
|
input.toggle-box id="administrator_tools" type="checkbox" checked=false
|
||||||
label for="administrator_tools" Manage Topic
|
label for="administrator_tools" Manage Topic
|
||||||
.toggle-box-container
|
.toggle-box-container
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
defmodule PhilomenaWeb.Image.CommentView do
|
defmodule PhilomenaWeb.Image.CommentView do
|
||||||
use PhilomenaWeb, :view
|
use PhilomenaWeb, :view
|
||||||
|
|
||||||
def anonymous_by_default?(conn) do
|
|
||||||
conn.assigns.current_user.anonymous_by_default
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -175,10 +175,6 @@ defmodule PhilomenaWeb.ImageView do
|
||||||
|
|
||||||
def scope(conn), do: PhilomenaWeb.ImageScope.scope(conn)
|
def scope(conn), do: PhilomenaWeb.ImageScope.scope(conn)
|
||||||
|
|
||||||
def anonymous_by_default?(conn) do
|
|
||||||
conn.assigns.current_user.anonymous_by_default
|
|
||||||
end
|
|
||||||
|
|
||||||
def info_row(_conn, []), do: []
|
def info_row(_conn, []), do: []
|
||||||
|
|
||||||
def info_row(conn, [{tag, description, dnp_entries}]) do
|
def info_row(conn, [{tag, description, dnp_entries}]) do
|
||||||
|
|
7
lib/philomena_web/views/markdown_view.ex
Normal file
7
lib/philomena_web/views/markdown_view.ex
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
defmodule PhilomenaWeb.MarkdownView do
|
||||||
|
use PhilomenaWeb, :view
|
||||||
|
|
||||||
|
def anonymous_by_default?(conn) do
|
||||||
|
conn.assigns.current_user.anonymous_by_default
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,3 +0,0 @@
|
||||||
defmodule PhilomenaWeb.TextileView do
|
|
||||||
use PhilomenaWeb, :view
|
|
||||||
end
|
|
|
@ -1,7 +1,3 @@
|
||||||
defmodule PhilomenaWeb.Topic.PostView do
|
defmodule PhilomenaWeb.Topic.PostView do
|
||||||
use PhilomenaWeb, :view
|
use PhilomenaWeb, :view
|
||||||
|
|
||||||
def anonymous_by_default?(conn) do
|
|
||||||
conn.assigns.current_user.anonymous_by_default
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,3 @@
|
||||||
defmodule PhilomenaWeb.TopicView do
|
defmodule PhilomenaWeb.TopicView do
|
||||||
use PhilomenaWeb, :view
|
use PhilomenaWeb, :view
|
||||||
|
|
||||||
def anonymous_by_default?(conn) do
|
|
||||||
conn.assigns.current_user.anonymous_by_default
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue