mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-30 14:57:59 +01:00
Merge pull request #137 from philomena-dev/markdown-frontend
Markdown-based text editor
This commit is contained in:
commit
77b2fb93bd
35 changed files with 424 additions and 482 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,7 @@ a.block__header--single-item, .block__header a {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block__content--top-border {
|
||||||
|
border-top: $border;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
250
assets/js/markdowntoolbar.js
Normal file
250
assets/js/markdowntoolbar.js
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
/**
|
||||||
|
* Markdown toolbar
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { $, $$ } 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: wrapSelectionOrLines,
|
||||||
|
options: {
|
||||||
|
prefix: '`',
|
||||||
|
suffix: '`',
|
||||||
|
prefixMultiline: '```\n',
|
||||||
|
suffixMultiline: '\n```',
|
||||||
|
singleWrap: true,
|
||||||
|
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' }
|
||||||
|
},
|
||||||
|
escape: {
|
||||||
|
action: escapeSelection,
|
||||||
|
options: { escapeChar: '\\' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSelections(textarea, linesOnly = false) {
|
||||||
|
let { selectionStart, selectionEnd } = textarea,
|
||||||
|
selection = textarea.value.substring(selectionStart, selectionEnd),
|
||||||
|
leadingSpace = '',
|
||||||
|
trailingSpace = '',
|
||||||
|
caret;
|
||||||
|
|
||||||
|
const processLinesOnly = linesOnly instanceof RegExp ? linesOnly.test(selection) : linesOnly;
|
||||||
|
if (processLinesOnly) {
|
||||||
|
const explorer = /\n/g;
|
||||||
|
let startNewlineIndex = 0,
|
||||||
|
endNewlineIndex = textarea.value.length;
|
||||||
|
while (explorer.exec(textarea.value)) {
|
||||||
|
const { lastIndex } = explorer;
|
||||||
|
if (lastIndex <= selectionStart) {
|
||||||
|
startNewlineIndex = lastIndex;
|
||||||
|
}
|
||||||
|
else if (lastIndex > selectionEnd) {
|
||||||
|
endNewlineIndex = lastIndex - 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectionStart = startNewlineIndex;
|
||||||
|
const startRemovedValue = textarea.value.substring(selectionStart);
|
||||||
|
const startsWithBlankString = startRemovedValue.match(/^[\s\n]+/);
|
||||||
|
if (startsWithBlankString) {
|
||||||
|
// Offset the selection start to the first non-blank line's first non-blank character, since
|
||||||
|
// Some browsers treat selection up to the start of the line as including the end of the
|
||||||
|
// previous line
|
||||||
|
selectionStart += startsWithBlankString[0].length;
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
processLinesOnly,
|
||||||
|
selectedText: selection,
|
||||||
|
beforeSelection: textarea.value.substring(0, selectionStart) + leadingSpace,
|
||||||
|
afterSelection: trailingSpace + textarea.value.substring(selectionEnd)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformSelection(textarea, transformer, eachLine) {
|
||||||
|
const { selectedText, beforeSelection, afterSelection, processLinesOnly } = getSelections(textarea, eachLine),
|
||||||
|
// For long comments, record scrollbar position to restore it later
|
||||||
|
{ scrollTop } = textarea;
|
||||||
|
|
||||||
|
const { newText, caretOffset } = transformer(selectedText, processLinesOnly);
|
||||||
|
|
||||||
|
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;
|
||||||
|
// Needed for automatic textarea resizing
|
||||||
|
textarea.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `](${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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newText = prefix + newText + suffix
|
||||||
|
|
||||||
|
return {
|
||||||
|
newText,
|
||||||
|
caretOffset: emptyText ? prefix.length : newText.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapLines(textarea, options, eachLine = true) {
|
||||||
|
transformSelection(textarea, (selectedText, processLinesOnly) => {
|
||||||
|
const { text = selectedText, singleWrap = false } = options,
|
||||||
|
prefix = (processLinesOnly && options.prefixMultiline) || options.prefix || '',
|
||||||
|
suffix = (processLinesOnly && options.suffixMultiline) || options.suffix || '',
|
||||||
|
emptyText = text === '';
|
||||||
|
let newText = singleWrap
|
||||||
|
? prefix + text.trim() + suffix
|
||||||
|
: text.split(/\n/g).map(line => prefix + line.trim() + suffix).join('\n');
|
||||||
|
|
||||||
|
return { newText, caretOffset: emptyText ? prefix.length : newText.length };
|
||||||
|
}, eachLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapSelectionOrLines(textarea, options) {
|
||||||
|
wrapLines(textarea, options, /\n/);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeSelection(textarea, options) {
|
||||||
|
transformSelection(textarea, selectedText => {
|
||||||
|
const { text = selectedText } = options,
|
||||||
|
emptyText = text === '';
|
||||||
|
|
||||||
|
if (emptyText) return;
|
||||||
|
|
||||||
|
const newText = text.replace(/([*_[\]()^`%\\~<>#|])/g, '\\$1');
|
||||||
|
|
||||||
|
return {
|
||||||
|
newText,
|
||||||
|
caretOffset: newText.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 toolbar
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { setupToolbar };
|
|
@ -1,9 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* 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 { 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>';
|
||||||
|
@ -35,43 +36,84 @@ function commentReply(user, url, textarea, quote) {
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPreview(body, anonymous, previewTab, isImage = false) {
|
function getPreview(body, anonymous, previewLoading, previewIdle, previewContent) {
|
||||||
let path = '/posts/preview';
|
const path = '/posts/preview';
|
||||||
|
|
||||||
|
if (typeof body !== 'string') return;
|
||||||
|
|
||||||
|
showEl(previewLoading);
|
||||||
|
hideEl(previewIdle);
|
||||||
|
|
||||||
fetchJson('POST', path, { body, anonymous })
|
fetchJson('POST', path, { body, anonymous })
|
||||||
.then(handleError)
|
.then(handleError)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
previewTab.innerHTML = data;
|
previewContent.innerHTML = data;
|
||||||
filterNode(previewTab);
|
filterNode(previewContent);
|
||||||
|
showEl(previewIdle);
|
||||||
|
hideEl(previewLoading);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 previewButton = document.querySelector('a[data-click-tab="preview"]');
|
||||||
const previewTab = document.querySelector('.block__tab[data-tab="preview"]');
|
const previewLoading = document.querySelector('.js-preview-loading');
|
||||||
const previewAnon = document.querySelector('.preview-anonymous') || false;
|
const previewIdle = document.querySelector('.js-preview-idle');
|
||||||
|
const previewContent = document.querySelector('.js-preview-content');
|
||||||
|
const previewAnon = document.querySelector('.js-preview-anonymous') || false;
|
||||||
|
|
||||||
if (!textarea || !previewButton) {
|
if (!textarea || !previewContent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
previewButton.addEventListener('click', () => {
|
const getCacheKey = () => {
|
||||||
if (previewTab.previewedText === textarea.value) return;
|
return (previewAnon && previewAnon.checked ? 'anon;' : '') + textarea.value;
|
||||||
previewTab.previewedText = textarea.value;
|
}
|
||||||
|
|
||||||
getPreview(textarea.value, Boolean(previewAnon.checked), previewTab, imageDesc);
|
const previewedTextAttribute = 'data-previewed-text';
|
||||||
});
|
const updatePreview = () => {
|
||||||
|
const cachedValue = getCacheKey()
|
||||||
|
if (previewContent.getAttribute(previewedTextAttribute) === cachedValue) return;
|
||||||
|
previewContent.setAttribute(previewedTextAttribute, cachedValue);
|
||||||
|
|
||||||
|
getPreview(textarea.value, previewAnon && previewAnon.checked, previewLoading, previewIdle, previewContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
previewButton.addEventListener('click', updatePreview);
|
||||||
|
textarea.addEventListener('change', resizeTextarea);
|
||||||
|
textarea.addEventListener('keyup', resizeTextarea);
|
||||||
|
|
||||||
|
// Fire handler for automatic resizing if textarea contains text on page load (e.g. editing)
|
||||||
|
if (textarea.value) textarea.dispatchEvent(new Event('change'));
|
||||||
|
|
||||||
previewAnon && previewAnon.addEventListener('click', () => {
|
previewAnon && previewAnon.addEventListener('click', () => {
|
||||||
getPreview(textarea.value, Boolean(previewAnon.checked), previewTab, imageDesc);
|
if (previewContent.classList.contains('hidden')) return;
|
||||||
|
|
||||||
|
updatePreview();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('click', event => {
|
document.addEventListener('click', event => {
|
||||||
|
|
|
@ -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 };
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
' 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
|
|
||||||
|
|
||||||
.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…")]
|
||||||
|
|
|
@ -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."
|
||||||
|
|
|
@ -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
|
17
lib/philomena_web/templates/markdown/_help.html.slime
Normal file
17
lib/philomena_web/templates/markdown/_help.html.slime
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
.editor-syntax-reference
|
||||||
|
strong<> Syntax quick reference:
|
||||||
|
|
||||||
|
span
|
||||||
|
strong> **bold**
|
||||||
|
em> *italic*
|
||||||
|
span.spoiler> ||hide text||
|
||||||
|
code> `code`
|
||||||
|
ins> __underline__
|
||||||
|
del> ~~strike~~
|
||||||
|
sup> ^sup^
|
||||||
|
sub %sub%
|
||||||
|
|
||||||
|
p
|
||||||
|
a href="/pages/markdown"
|
||||||
|
i.fa.fa-question-circle>
|
||||||
|
strong Detailed syntax guide
|
24
lib/philomena_web/templates/markdown/_input.html.slime
Normal file
24
lib/philomena_web/templates/markdown/_input.html.slime
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
- 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__header.block__header--js-tabbed
|
||||||
|
a.selected href="#" data-click-tab="write"
|
||||||
|
i.fa> class="fa-#{action_icon}"
|
||||||
|
= action_text
|
||||||
|
|
||||||
|
a href="#" data-click-tab="preview"
|
||||||
|
i.fa.fa-cog.fa-fw.fa-spin.js-preview-loading.hidden> title=raw('Loading preview…')
|
||||||
|
i.fa.fa-eye.fa-fw.js-preview-idle>
|
||||||
|
| Preview
|
||||||
|
|
||||||
|
.block__tab.communication-edit__tab.selected.js-preview-input-wrapper data-tab="write"
|
||||||
|
= 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__tab.communication-edit__tab.hidden.js-preview-content data-tab="preview"
|
|
@ -29,6 +29,6 @@
|
||||||
i.fa.fa-link
|
i.fa.fa-link
|
||||||
button.communication__toolbar__button tabindex="-1" type="button" title="insert image (ctrl+k)" data-syntax-id="image"
|
button.communication__toolbar__button tabindex="-1" type="button" title="insert image (ctrl+k)" data-syntax-id="image"
|
||||||
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="escape"
|
||||||
span
|
span
|
||||||
| no parse
|
| escape
|
|
@ -2,7 +2,7 @@
|
||||||
.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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
.textile-syntax-reference
|
|
||||||
strong<> Syntax quick reference:
|
|
||||||
|
|
||||||
span
|
|
||||||
strong>
|
|
||||||
' *bold*
|
|
||||||
em> _italic_
|
|
||||||
span.spoiler>
|
|
||||||
| [spoiler]hide text[/spoiler]
|
|
||||||
code> @code@
|
|
||||||
ins> +underline+
|
|
||||||
del> -strike-
|
|
||||||
sup> ^sup^
|
|
||||||
sub ~sub~
|
|
||||||
|
|
||||||
button< class="button" type="button" data-click-toggle="button:hover + .textile_help, button:focus + .textile_help"
|
|
||||||
' …
|
|
||||||
|
|
||||||
p.hidden.textile_help
|
|
||||||
' [==stuff you don't want textile to parse==]
|
|
||||||
br
|
|
||||||
|
|
||||||
ins> Links:
|
|
||||||
' "On-site link":/some-link, "External link":http://some-link
|
|
||||||
br
|
|
||||||
|
|
||||||
ins> Images:
|
|
||||||
' >>1 — link to image, >>1t — embed image thumbnail (>>1p — large preview, >>1s — small thumbnail)
|
|
||||||
br
|
|
||||||
|
|
||||||
ins> External images:
|
|
||||||
' !http://some-image!, !http://some-clickable-image!:http://some-link
|
|
||||||
br
|
|
||||||
|
|
||||||
strong> Remember to use embeds (>>) for booru images as these let users filter content they don't want to see
|
|
|
@ -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.
|
||||||
|
|
||||||
.block
|
h1 Create a Topic
|
||||||
.block__header.block__header--js-tabbed
|
|
||||||
a.selected href="#" data-click-tab="write"
|
|
||||||
i.fas.fa-pencil-alt>
|
|
||||||
' Create a Topic
|
|
||||||
|
|
||||||
a href="#" data-click-tab="preview"
|
|
||||||
i.fa.fa-eye>
|
|
||||||
' Preview
|
|
||||||
|
|
||||||
.block__tab.communication-edit__tab.selected data-tab="write"
|
|
||||||
.field
|
.field
|
||||||
= text_input f, :title, class: "input input--wide", placeholder: "Title"
|
= text_input f, :title, class: "input input--wide", placeholder: "Title"
|
||||||
= error_tag f, :title
|
= error_tag f, :title
|
||||||
= error_tag f, :slug
|
= error_tag f, :slug
|
||||||
|
|
||||||
|
.block
|
||||||
= inputs_for f, :posts, fn fp ->
|
= inputs_for f, :posts, fn fp ->
|
||||||
.field
|
= 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."
|
||||||
= 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
|
.block__content.communication-edit__wrap
|
||||||
.field
|
= render PhilomenaWeb.MarkdownView, "_anon_checkbox.html", conn: @conn, f: f, label: "Post anonymously"
|
||||||
=> 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…")]
|
||||||
|
|
|
@ -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