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 {
|
||||
margin: 1em 2em;
|
||||
border: 1px dotted $foreground_color;
|
||||
|
@ -298,7 +298,7 @@ blockquote blockquote blockquote blockquote blockquote blockquote {
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.textile-syntax-reference {
|
||||
.editor-syntax-reference {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
|
|
|
@ -222,3 +222,50 @@ a.block__header--single-item, .block__header a {
|
|||
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;
|
||||
}
|
||||
|
||||
.input--resize-vertical {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin: 0.2em 0 0 0.4em;
|
||||
padding: 0;
|
||||
|
|
|
@ -51,7 +51,7 @@ span.communication__sender__stats,
|
|||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.communication-edit__tab {
|
||||
.communication-edit__wrap {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ span.communication__sender__stats,
|
|||
}
|
||||
}
|
||||
|
||||
.hyphenate-breaks{
|
||||
.hyphenate-breaks {
|
||||
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 { filterNode } from './imagesclientside';
|
||||
import { debounce } from './utils/events.js';
|
||||
import { hideEl, showEl } from './utils/dom.js';
|
||||
|
||||
function handleError(response) {
|
||||
const errorMessage = '<div>Preview failed to load!</div>';
|
||||
|
||||
if (!response.ok) {
|
||||
if (!response.ok){
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
|
@ -22,7 +24,7 @@ function commentReply(user, url, textarea, quote) {
|
|||
if (newval && /\n$/.test(newval)) newval += '\n';
|
||||
newval += `${text}\n`;
|
||||
|
||||
if (quote) {
|
||||
if (quote){
|
||||
newval += `[bq="${user.replace('"', '\'')}"] ${quote} [/bq]\n`;
|
||||
}
|
||||
|
||||
|
@ -35,47 +37,99 @@ function commentReply(user, url, textarea, quote) {
|
|||
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';
|
||||
|
||||
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(data => {
|
||||
previewTab.innerHTML = data;
|
||||
filterNode(previewTab);
|
||||
previewContent.innerHTML = data;
|
||||
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() {
|
||||
let textarea = document.querySelector('.js-preview-input');
|
||||
let imageDesc = false;
|
||||
|
||||
if (!textarea) {
|
||||
if (!textarea){
|
||||
textarea = document.querySelector('.js-preview-description');
|
||||
imageDesc = true;
|
||||
}
|
||||
|
||||
const previewButton = document.querySelector('a[data-click-tab="preview"]');
|
||||
const previewTab = document.querySelector('.block__tab[data-tab="preview"]');
|
||||
const previewAnon = document.querySelector('.preview-anonymous') || false;
|
||||
const previewLoading = document.querySelector('.communication-preview__loading');
|
||||
const previewContent = document.querySelector('.communication-preview__content');
|
||||
const previewAnon = document.querySelector('.js-preview-anonymous') || false;
|
||||
|
||||
if (!textarea || !previewButton) {
|
||||
if (!textarea || !previewContent){
|
||||
return;
|
||||
}
|
||||
|
||||
previewButton.addEventListener('click', () => {
|
||||
if (previewTab.previewedText === textarea.value) return;
|
||||
previewTab.previewedText = textarea.value;
|
||||
const updatePreview = () => {
|
||||
getPreview(textarea.value, previewAnon && previewAnon.checked, previewLoading, previewContent);
|
||||
};
|
||||
|
||||
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', () => {
|
||||
getPreview(textarea.value, Boolean(previewAnon.checked), previewTab, imageDesc);
|
||||
});
|
||||
textarea.addEventListener('keydown', debouncedUpdater);
|
||||
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 => {
|
||||
if (event.target && event.target.closest('.post-reply')) {
|
||||
if (event.target && event.target.closest('.post-reply')){
|
||||
const link = event.target.closest('.post-reply');
|
||||
commentReply(link.dataset.author, link.getAttribute('href'), textarea, link.dataset.post);
|
||||
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
|
||||
*/
|
||||
|
||||
function fetchJson(verb, endpoint, body) {
|
||||
function fetchJson(verb, endpoint, body, signal) {
|
||||
const data = {
|
||||
method: verb,
|
||||
credentials: 'same-origin',
|
||||
|
@ -11,6 +11,7 @@ function fetchJson(verb, endpoint, body) {
|
|||
'x-csrf-token': window.booru.csrfToken,
|
||||
'x-requested-with': 'xmlhttprequest'
|
||||
},
|
||||
signal,
|
||||
};
|
||||
|
||||
if (body) {
|
||||
|
|
|
@ -31,7 +31,7 @@ import { setupTagEvents } from './tagsmisc';
|
|||
import { setupTimestamps } from './timeago';
|
||||
import { setupImageUpload } from './upload';
|
||||
import { setupSearch } from './search';
|
||||
import { setupToolbar } from './textiletoolbar';
|
||||
import { setupToolbar } from './markdowntoolbar.js';
|
||||
import { hideStaffTools } from './staffhider';
|
||||
import { pollOptionCreator } from './poll';
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ article.block.communication id="comment_#{@comment.id}"
|
|||
.flex__fixed.spacing-right
|
||||
.post-image-container
|
||||
= render PhilomenaWeb.ImageView, "_image_container.html", image: @comment.image, size: :thumb_tiny, conn: @conn
|
||||
|
||||
|
||||
.flex__grow.communication__body
|
||||
span.communication__body__sender-name = render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @comment, awards: true, conn: @conn
|
||||
br
|
||||
|
|
|
@ -1,24 +1,7 @@
|
|||
= form_for @changeset, Routes.conversation_message_path(@conn, :create, @conversation), fn f ->
|
||||
.block
|
||||
.block__header.block__header--js-tabbed
|
||||
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
|
||||
|
||||
.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...]
|
||||
.communication-edit__wrap
|
||||
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, action_icon: "pencil-alt", action_text: "Reply"
|
||||
|
||||
.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
|
||||
|
||||
= inputs_for f, :messages, fn fm ->
|
||||
.block
|
||||
.block__header.block__header--js-tabbed
|
||||
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...]
|
||||
div
|
||||
= render PhilomenaWeb.MarkdownView, "_input.html", changeset: @changeset, conn: @conn, f: fm, action_icon: "pencil-alt", action_text: "Compose"
|
||||
|
||||
.block__content.communication-edit__actions
|
||||
= submit "Send", class: "button", autocomplete: "off", data: [disable_with: "Sending..."]
|
||||
|
|
|
@ -6,29 +6,10 @@
|
|||
p Oops, something went wrong! Please check the errors below.
|
||||
|
||||
.block
|
||||
.block__header.block__header--js-tabbed
|
||||
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, :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...]
|
||||
div
|
||||
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, placeholder: "Please read the site rules before posting and use ||spoilers|| for above-rating stuff."
|
||||
|
||||
.block__content.communication-edit__actions
|
||||
=> submit "Post", class: "button", data: [disable_with: raw("Posting…")]
|
||||
|
||||
= if @conn.assigns.current_user do
|
||||
= checkbox f, :anonymous, value: anonymous_by_default?(@conn)
|
||||
= label f, :anonymous, "Anonymous"
|
||||
= render PhilomenaWeb.MarkdownView, "_anon_checkbox.html", conn: @conn, f: f
|
||||
|
|
|
@ -4,29 +4,12 @@
|
|||
p Oops, something went wrong! Please check the errors below.
|
||||
|
||||
.block
|
||||
.block__header.block__header--js-tabbed
|
||||
a.selected href="#" data-click-tab="write"
|
||||
i.fas.fa-edit>
|
||||
' Edit
|
||||
.communication-edit__wrap
|
||||
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, placeholder: "Please read the site rules before posting and use ||spoilers|| for above-rating stuff."
|
||||
|
||||
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
|
||||
|
||||
.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.field
|
||||
= text_input f, :edit_reason, class: "input input--wide", placeholder: "Reason for edit"
|
||||
= error_tag f, :edit_reason
|
||||
|
||||
.block__content.communication-edit__actions
|
||||
=> submit "Edit", class: "button", data: [disable_with: raw("Posting…")]
|
||||
|
|
|
@ -43,4 +43,4 @@ h1
|
|||
' Edited
|
||||
=> pretty_time(version.created_at)
|
||||
' 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
|
||||
p Oops, something went wrong! Please check the errors below.
|
||||
|
||||
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
|
||||
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
|
||||
= render PhilomenaWeb.MarkdownView, "_help.html", conn: @conn
|
||||
= render PhilomenaWeb.MarkdownView, "_toolbar.html", conn: @conn
|
||||
|
||||
.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."
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
.field
|
||||
= 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"
|
||||
|
||||
|
||||
.field
|
||||
label for="image[tag_input]"
|
||||
' Describe with
|
||||
|
@ -64,24 +64,12 @@
|
|||
|
||||
.field
|
||||
.block
|
||||
.block__header.block__header--js-tabbed
|
||||
= link "Description", to: "#", class: "selected", data: [click_tab: "write"]
|
||||
= link "Preview", to: "#", data: [click_tab: "preview"]
|
||||
.communication-edit__wrap
|
||||
= 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."
|
||||
|
||||
.block__tab.selected data-tab="write"
|
||||
= 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.MarkdownView, "_anon_checkbox.html", conn: @conn, f: f, label: "Post anonymously"
|
||||
|
||||
= render PhilomenaWeb.CaptchaView, "_captcha.html", name: "image", conn: @conn
|
||||
|
||||
.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:
|
||||
|
||||
span
|
||||
strong>
|
||||
' *bold*
|
||||
em> _italic_
|
||||
span.spoiler>
|
||||
| [spoiler]hide text[/spoiler]
|
||||
code> @code@
|
||||
ins> +underline+
|
||||
del> -strike-
|
||||
strong> **bold**
|
||||
em> *italic*
|
||||
span.spoiler> ||hide text||
|
||||
code> `code`
|
||||
ins> __underline__
|
||||
del> ~~strike~~
|
||||
sup> ^sup^
|
||||
sub ~sub~
|
||||
|
||||
|
@ -17,11 +15,11 @@
|
|||
' …
|
||||
|
||||
p.hidden.textile_help
|
||||
' [==stuff you don't want textile to parse==]
|
||||
' Escape formatting using \ e.g. \* for a literal star character
|
||||
br
|
||||
|
||||
ins> Links:
|
||||
' "On-site link":/some-link, "External link":http://some-link
|
||||
' [On-site link](/some-link), [External link](http://some-link)
|
||||
br
|
||||
|
||||
ins> Images:
|
||||
|
@ -29,7 +27,7 @@
|
|||
br
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
button.communication__toolbar__button tabindex="-1" type="button" title="Text you want the parser to ignore" data-syntax-id="noParse"
|
||||
span
|
||||
| no parse
|
||||
| escape
|
|
@ -2,9 +2,9 @@
|
|||
.flex__fixed.spacing-right
|
||||
= render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @post, conn: @conn
|
||||
|
||||
.flex__grow.communication_body
|
||||
.flex__grow.communication__body
|
||||
span.communication__body__sender-name
|
||||
= render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @post, conn: @conn, awards: true
|
||||
|
||||
.communication__body__text
|
||||
== @body
|
||||
== @body
|
||||
|
|
|
@ -14,25 +14,8 @@ h1 Updating Profile Description
|
|||
= error_tag f, :personal_title
|
||||
|
||||
.block
|
||||
.block__header.block__header--js-tabbed
|
||||
a.selected href="#" data-click-tab="write"
|
||||
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...]
|
||||
div
|
||||
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, action_text: "About Me", placeholder: "Description (up to 10000 characters)", name: :description
|
||||
|
||||
.block__content.communication-edit__actions
|
||||
=> submit "Update", class: "button"
|
||||
|
|
|
@ -5,25 +5,8 @@ h1 Updating Moderation Scratchpad
|
|||
.alert.alert-danger
|
||||
p Oops, something went wrong! Please check the errors below.
|
||||
.block
|
||||
.block__header.block__header--js-tabbed
|
||||
a.selected href="#" data-click-tab="write"
|
||||
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...]
|
||||
div
|
||||
= render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, action_text: "Scratchpad", placeholder: "Scratchpad Contents", name: :scratchpad
|
||||
|
||||
.block__content.communication-edit__actions
|
||||
=> submit "Update", class: "button"
|
||||
|
|
|
@ -45,24 +45,7 @@ p
|
|||
.field
|
||||
= select f, :category, report_categories(), class: "input"
|
||||
.block
|
||||
.block__header.block__header--js-tabbed
|
||||
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.MarkdownView, "_input.html", conn: @conn, f: f, placeholder: "Provide anything else we should know here.", name: :reason
|
||||
|
||||
= render PhilomenaWeb.CaptchaView, "_captcha.html", name: "report", conn: @conn
|
||||
|
||||
|
|
|
@ -3,43 +3,26 @@
|
|||
.alert.alert-danger
|
||||
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__header.block__header--js-tabbed
|
||||
a.selected href="#" data-click-tab="write"
|
||||
i.fas.fa-pencil-alt>
|
||||
' Create a Topic
|
||||
= inputs_for f, :posts, fn fp ->
|
||||
= 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."
|
||||
|
||||
a href="#" data-click-tab="preview"
|
||||
i.fa.fa-eye>
|
||||
' 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"
|
||||
.block__content.communication-edit__wrap
|
||||
= render PhilomenaWeb.MarkdownView, "_anon_checkbox.html", conn: @conn, f: f, label: "Post anonymously"
|
||||
|
||||
= inputs_for f, :poll, fn fp ->
|
||||
#add-poll
|
||||
#add-poll.field
|
||||
input.toggle-box id="add_poll" name="add_poll" type="checkbox"
|
||||
label for="add_poll" Add a poll
|
||||
.toggle-box-container
|
||||
= 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
|
||||
= submit "Post", class: "button", data: [disable_with: raw("Posting…")]
|
||||
|
|
|
@ -4,29 +4,10 @@
|
|||
p Oops, something went wrong! Please check the errors below.
|
||||
|
||||
.block
|
||||
.block__header.block__header--js-tabbed
|
||||
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, :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...]
|
||||
div
|
||||
= 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."
|
||||
|
||||
.block__content.communication-edit__actions
|
||||
=> submit "Post", class: "button", data: [disable_with: raw("Posting…")]
|
||||
|
||||
= if @conn.assigns.current_user do
|
||||
= checkbox f, :anonymous, value: anonymous_by_default?(@conn)
|
||||
= label f, :anonymous, "Anonymous"
|
||||
= render PhilomenaWeb.MarkdownView, "_anon_checkbox.html", conn: @conn, f: f
|
||||
|
|
|
@ -4,29 +4,13 @@
|
|||
p Oops, something went wrong! Please check the errors below.
|
||||
|
||||
.block
|
||||
.block__header.block__header--js-tabbed
|
||||
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
|
||||
|
||||
.communication-edit__wrap
|
||||
.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
|
||||
= 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."
|
||||
|
||||
.field
|
||||
.block__content.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
|
||||
=> submit "Edit", class: "button", data: [disable_with: raw("Posting…")]
|
||||
|
|
|
@ -42,4 +42,4 @@ h1
|
|||
' Edited
|
||||
=> pretty_time(version.created_at)
|
||||
' 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 ->
|
||||
|
||||
= if can?(@conn, :hide, @topic) do
|
||||
.block__content
|
||||
.block__content.block__content--top-border
|
||||
input.toggle-box id="administrator_tools" type="checkbox" checked=false
|
||||
label for="administrator_tools" Manage Topic
|
||||
.toggle-box-container
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
defmodule PhilomenaWeb.Image.CommentView do
|
||||
use PhilomenaWeb, :view
|
||||
|
||||
def anonymous_by_default?(conn) do
|
||||
conn.assigns.current_user.anonymous_by_default
|
||||
end
|
||||
end
|
||||
|
|
|
@ -175,10 +175,6 @@ defmodule PhilomenaWeb.ImageView do
|
|||
|
||||
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, [{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
|
||||
use PhilomenaWeb, :view
|
||||
|
||||
def anonymous_by_default?(conn) do
|
||||
conn.assigns.current_user.anonymous_by_default
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
defmodule PhilomenaWeb.TopicView do
|
||||
use PhilomenaWeb, :view
|
||||
|
||||
def anonymous_by_default?(conn) do
|
||||
conn.assigns.current_user.anonymous_by_default
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue