initial implementation of markdown-based text editor

This commit is contained in:
SeinopSys 2021-09-13 01:19:00 +02:00
parent 5bfcf56728
commit 71fa95e462
No known key found for this signature in database
GPG key ID: 9BFB053C1BA6C5C4
37 changed files with 511 additions and 468 deletions

View file

@ -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;
} }

View file

@ -222,3 +222,50 @@ a.block__header--single-item, .block__header a {
display: none; display: none;
} }
} }
.block__content--top-border {
border-top: $border;
}
.block__column__header {
font-size: 14px;
line-height: $block_header_sub_height;
margin-bottom: $header_field_spacing;
border-bottom: 1px solid;
font-weight: bold;
}
@media (min-width: $min_px_width_for_desktop_layout) {
.block--split {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
}
.block__column--full, .block__column--half {
flex-grow: 0;
flex-shrink: 0;
box-sizing: border-box;
}
.block__column--full {
flex-basis: 100%;
}
.block__column--half {
flex-basis: 50%;
max-width: 50%;
&:first-child {
padding-right: $block-spacing;
}
&:last-child {
padding-left: $block-spacing;
}
.flex__grow {
flex: 1;
min-width: 0;
}
}
}

View file

@ -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;

View file

@ -51,7 +51,7 @@ span.communication__sender__stats,
margin-left: 6px; margin-left: 6px;
} }
.communication-edit__tab { .communication-edit__wrap {
padding-bottom: 12px; padding-bottom: 12px;
} }
@ -89,7 +89,7 @@ span.communication__sender__stats,
} }
} }
.hyphenate-breaks{ .hyphenate-breaks {
hyphens: auto; hyphens: auto;
} }

View 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 };

View file

@ -1,14 +1,16 @@
/** /**
* Textile previews (posts, comments, messages) * Markdown previews (posts, comments, messages)
*/ */
import { fetchJson } from './utils/requests'; import { fetchJson } from './utils/requests';
import { filterNode } from './imagesclientside'; import { filterNode } from './imagesclientside';
import { debounce } from './utils/events.js';
import { hideEl, showEl } from './utils/dom.js';
function handleError(response) { function handleError(response) {
const errorMessage = '<div>Preview failed to load!</div>'; const errorMessage = '<div>Preview failed to load!</div>';
if (!response.ok) { if (!response.ok){
return errorMessage; return errorMessage;
} }
@ -22,7 +24,7 @@ function commentReply(user, url, textarea, quote) {
if (newval && /\n$/.test(newval)) newval += '\n'; if (newval && /\n$/.test(newval)) newval += '\n';
newval += `${text}\n`; newval += `${text}\n`;
if (quote) { if (quote){
newval += `[bq="${user.replace('"', '\'')}"] ${quote} [/bq]\n`; newval += `[bq="${user.replace('"', '\'')}"] ${quote} [/bq]\n`;
} }
@ -35,47 +37,99 @@ function commentReply(user, url, textarea, quote) {
textarea.focus(); textarea.focus();
} }
function getPreview(body, anonymous, previewTab, isImage = false) { /**
* Stores the abort controller for the current preview request
* @type {null|AbortController}
*/
let previewAbortController = null;
function getPreview(body, anonymous, previewLoading, previewContent) {
let path = '/posts/preview'; let path = '/posts/preview';
fetchJson('POST', path, { body, anonymous }) if (typeof body !== 'string') return;
const trimmedBody = body.trim();
if (trimmedBody.length < 1){
previewContent.innerHTML = '';
return;
}
showEl(previewLoading);
// Abort previous requests if it exists
if (previewAbortController) previewAbortController.abort();
previewAbortController = new AbortController();
fetchJson('POST', path, { body, anonymous }, previewAbortController.signal)
.then(handleError) .then(handleError)
.then(data => { .then(data => {
previewTab.innerHTML = data; previewContent.innerHTML = data;
filterNode(previewTab); filterNode(previewContent);
showEl(previewContent);
hideEl(previewLoading);
})
.finally(() => {
previewAbortController = null;
}); });
} }
/**
* Resizes the event target <textarea> to match the size of its contained text, between set
* minimum and maximum height values. Former comes from CSS, latter is hard coded below.
* @template {{ target: HTMLTextAreaElement }} E
* @param {E} e
*/
function resizeTextarea(e) {
// Reset inline height for fresh calculations
e.target.style.height = '';
const { borderTopWidth, borderBottomWidth, height } = window.getComputedStyle(e.target);
// Add scrollHeight and borders (because border-box) to get the target size that avoids scrollbars
const contentHeight = e.target.scrollHeight + parseFloat(borderTopWidth) + parseFloat(borderBottomWidth);
// Get the original default height provided by page styles
const regularHeight = parseFloat(height);
// Limit textarea's size to between the original height and 1000px
const newHeight = Math.max(regularHeight, Math.min(1000, contentHeight));
e.target.style.height = `${newHeight}px`;
}
function setupPreviews() { function setupPreviews() {
let textarea = document.querySelector('.js-preview-input'); let textarea = document.querySelector('.js-preview-input');
let imageDesc = false;
if (!textarea) { if (!textarea){
textarea = document.querySelector('.js-preview-description'); textarea = document.querySelector('.js-preview-description');
imageDesc = true;
} }
const previewButton = document.querySelector('a[data-click-tab="preview"]'); const previewLoading = document.querySelector('.communication-preview__loading');
const previewTab = document.querySelector('.block__tab[data-tab="preview"]'); const previewContent = document.querySelector('.communication-preview__content');
const previewAnon = document.querySelector('.preview-anonymous') || false; const previewAnon = document.querySelector('.js-preview-anonymous') || false;
if (!textarea || !previewButton) { if (!textarea || !previewContent){
return; return;
} }
previewButton.addEventListener('click', () => { const updatePreview = () => {
if (previewTab.previewedText === textarea.value) return; getPreview(textarea.value, previewAnon && previewAnon.checked, previewLoading, previewContent);
previewTab.previewedText = textarea.value; };
getPreview(textarea.value, Boolean(previewAnon.checked), previewTab, imageDesc); const debouncedUpdater = debounce(500, () => {
if (previewContent.previewedText === textarea.value) return;
previewContent.previewedText = textarea.value;
updatePreview();
}); });
previewAnon && previewAnon.addEventListener('click', () => { textarea.addEventListener('keydown', debouncedUpdater);
getPreview(textarea.value, Boolean(previewAnon.checked), previewTab, imageDesc); textarea.addEventListener('focus', debouncedUpdater);
}); textarea.addEventListener('change', resizeTextarea);
textarea.addEventListener('keyup', resizeTextarea);
// Fire handler if textarea contains text on page load (e.g. editing)
if (textarea.value) textarea.dispatchEvent(new Event('keydown'));
previewAnon && previewAnon.addEventListener('click', updatePreview);
document.addEventListener('click', event => { document.addEventListener('click', event => {
if (event.target && event.target.closest('.post-reply')) { if (event.target && event.target.closest('.post-reply')){
const link = event.target.closest('.post-reply'); const link = event.target.closest('.post-reply');
commentReply(link.dataset.author, link.getAttribute('href'), textarea, link.dataset.post); commentReply(link.dataset.author, link.getAttribute('href'), textarea, link.dataset.post);
event.preventDefault(); event.preventDefault();
@ -83,4 +137,4 @@ function setupPreviews() {
}); });
} }
export { setupPreviews }; export { setupPreviews };

View file

@ -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 };

View file

@ -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);
};
}

View file

@ -2,7 +2,7 @@
* Request Utils * Request Utils
*/ */
function fetchJson(verb, endpoint, body) { function fetchJson(verb, endpoint, body, signal) {
const data = { const data = {
method: verb, method: verb,
credentials: 'same-origin', credentials: 'same-origin',
@ -11,6 +11,7 @@ function fetchJson(verb, endpoint, body) {
'x-csrf-token': window.booru.csrfToken, 'x-csrf-token': window.booru.csrfToken,
'x-requested-with': 'xmlhttprequest' 'x-requested-with': 'xmlhttprequest'
}, },
signal,
}; };
if (body) { if (body) {

View file

@ -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';

View file

@ -3,7 +3,7 @@ article.block.communication id="comment_#{@comment.id}"
.flex__fixed.spacing-right .flex__fixed.spacing-right
.post-image-container .post-image-container
= render PhilomenaWeb.ImageView, "_image_container.html", image: @comment.image, size: :thumb_tiny, conn: @conn = render PhilomenaWeb.ImageView, "_image_container.html", image: @comment.image, size: :thumb_tiny, conn: @conn
.flex__grow.communication__body .flex__grow.communication__body
span.communication__body__sender-name = render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @comment, awards: true, conn: @conn span.communication__body__sender-name = render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @comment, awards: true, conn: @conn
br br

View file

@ -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&hellip;")]

View file

@ -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..."]

View file

@ -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&hellip;")] => submit "Post", class: "button", data: [disable_with: raw("Posting&hellip;")]
= 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"

View file

@ -4,29 +4,12 @@
p Oops, something went wrong! Please check the errors below. p Oops, something went wrong! Please check the errors below.
.block .block
.block__header.block__header--js-tabbed .communication-edit__wrap
a.selected href="#" data-click-tab="write" = render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, placeholder: "Please read the site rules before posting and use ||spoilers|| for above-rating stuff."
i.fas.fa-edit>
' Edit
a href="#" data-click-tab="preview" .block__content.field
i.fa.fa-eye> = text_input f, :edit_reason, class: "input input--wide", placeholder: "Reason for edit"
' Preview = error_tag f, :edit_reason
.block__tab.communication-edit__tab.selected data-tab="write"
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
.field
= textarea f, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Please read the site rules before posting and use [spoiler][/spoiler] for above-rating stuff.", required: true
= error_tag f, :body
.field
= text_input f, :edit_reason, class: "input input--wide", placeholder: "Reason for edit"
= error_tag f, :edit_reason
.block__tab.communication-edit__tab.hidden data-tab="preview"
' [Loading preview...]
.block__content.communication-edit__actions .block__content.communication-edit__actions
=> submit "Edit", class: "button", data: [disable_with: raw("Posting&hellip;")] => submit "Edit", class: "button", data: [disable_with: raw("Posting&hellip;")]

View file

@ -43,4 +43,4 @@ h1
' Edited ' Edited
=> pretty_time(version.created_at) => pretty_time(version.created_at)
' by ' by
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: version, conn: @conn => render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: version, conn: @conn

View file

@ -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."

View file

@ -41,7 +41,7 @@
.field .field
= label f, :source_url, "The page you found this image on" = label f, :source_url, "The page you found this image on"
= url_input f, :source_url, class: "input input--wide js-image-input", placeholder: "Source URL" = url_input f, :source_url, class: "input input--wide js-image-input", placeholder: "Source URL"
.field .field
label for="image[tag_input]" label for="image[tag_input]"
' Describe with ' Describe with
@ -64,24 +64,12 @@
.field .field
.block .block
.block__header.block__header--js-tabbed .communication-edit__wrap
= link "Description", to: "#", class: "selected", data: [click_tab: "write"] = render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: f, action_icon: "pencil-alt", action_text: "Description", placeholder: "Describe this image in plain words - this should generally be info about the image that doesn't belong in the tags or source."
= link "Preview", to: "#", data: [click_tab: "preview"]
.block__tab.selected data-tab="write" = render PhilomenaWeb.MarkdownView, "_anon_checkbox.html", conn: @conn, f: f, label: "Post anonymously"
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
= textarea f, :description, class: "input input--wide input--text js-preview-description js-image-input js-toolbar-input", placeholder: "Describe this image in plain words - this should generally be info about the image that doesn't belong in the tags or source."
.block__tab.hidden data-tab="preview"
| Loading preview...
= if @conn.assigns.current_user do
.field
= label f, :anonymous, "Post anonymously"
= checkbox f, :anonymous, class: "checkbox", value: anonymous_by_default?(@conn)
= render PhilomenaWeb.CaptchaView, "_captcha.html", name: "image", conn: @conn = render PhilomenaWeb.CaptchaView, "_captcha.html", name: "image", conn: @conn
.actions .actions
= submit "Upload", class: "button", autocomplete: "off", data: [disable_with: "Please wait..."] = submit "Upload", class: "button input--separate-top", autocomplete: "off", data: [disable_with: "Please wait..."]

View file

@ -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

View file

@ -1,15 +1,13 @@
.textile-syntax-reference .editor-syntax-reference
strong<> Syntax quick reference: strong<> Syntax quick reference:
span span
strong> strong> **bold**
' *bold* em> *italic*
em> _italic_ span.spoiler> ||hide text||
span.spoiler> code> `code`
| [spoiler]hide text[/spoiler] ins> __underline__
code> @code@ del> ~~strike~~
ins> +underline+
del> -strike-
sup> ^sup^ sup> ^sup^
sub ~sub~ sub ~sub~
@ -17,11 +15,11 @@
' &hellip; ' &hellip;
p.hidden.textile_help p.hidden.textile_help
' [==stuff you don't want textile to parse==] ' Escape formatting using \ e.g. \* for a literal star character
br br
ins> Links: ins> Links:
' "On-site link":/some-link, "External link":http://some-link ' [On-site link](/some-link), [External link](http://some-link)
br br
ins> Images: ins> Images:
@ -29,7 +27,7 @@
br br
ins> External images: ins> External images:
' !http://some-image!, !http://some-clickable-image!:http://some-link ' ![alt text](http://some-image), [![alt text](http://some-clickable-image)](http://some-link)
br br
strong> Remember to use embeds (&gt;&gt;) for booru images as these let users filter content they don't want to see strong> Remember to use embeds (&gt;&gt;) for booru images as these let users filter content they don't want to see

View 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&hellip;')
.communication-preview__content

View file

@ -31,4 +31,4 @@
i.fa.fa-image i.fa.fa-image
button.communication__toolbar__button tabindex="-1" type="button" title="Text you want the parser to ignore" data-syntax-id="noParse" button.communication__toolbar__button tabindex="-1" type="button" title="Text you want the parser to ignore" data-syntax-id="noParse"
span span
| no parse | escape

View file

@ -2,9 +2,9 @@
.flex__fixed.spacing-right .flex__fixed.spacing-right
= render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @post, conn: @conn = render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @post, conn: @conn
.flex__grow.communication_body .flex__grow.communication__body
span.communication__body__sender-name span.communication__body__sender-name
= render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @post, conn: @conn, awards: true = render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @post, conn: @conn, awards: true
.communication__body__text .communication__body__text
== @body == @body

View file

@ -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"

View file

@ -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"

View file

@ -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

View file

@ -3,43 +3,26 @@
.alert.alert-danger .alert.alert-danger
p Oops, something went wrong! Please check the errors below. p Oops, something went wrong! Please check the errors below.
h1 Create a Topic
.field
= text_input f, :title, class: "input input--wide", placeholder: "Title"
= error_tag f, :title
= error_tag f, :slug
.block .block
.block__header.block__header--js-tabbed = inputs_for f, :posts, fn fp ->
a.selected href="#" data-click-tab="write" = render PhilomenaWeb.MarkdownView, "_input.html", conn: @conn, f: fp, action_icon: "pencil-alt", action_text: "First Post", placeholder: "Please read the site rules before posting and use ||spoilers|| for NSFW stuff in SFW forums."
i.fas.fa-pencil-alt>
' Create a Topic
a href="#" data-click-tab="preview" .block__content.communication-edit__wrap
i.fa.fa-eye> = render PhilomenaWeb.MarkdownView, "_anon_checkbox.html", conn: @conn, f: f, label: "Post anonymously"
' Preview
.block__tab.communication-edit__tab.selected data-tab="write"
.field
= text_input f, :title, class: "input input--wide", placeholder: "Title"
= error_tag f, :title
= error_tag f, :slug
= inputs_for f, :posts, fn fp ->
.field
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
= textarea fp, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Please read the site rules before posting and use [spoiler][/spoiler] for NSFW stuff in SFW forums.", required: true
= error_tag fp, :body
= if @conn.assigns.current_user do
.field
=> checkbox f, :anonymous, value: anonymous_by_default?(@conn)
= label f, :anonymous, "Post anonymously"
= inputs_for f, :poll, fn fp -> = inputs_for f, :poll, fn fp ->
#add-poll #add-poll.field
input.toggle-box id="add_poll" name="add_poll" type="checkbox" input.toggle-box id="add_poll" name="add_poll" type="checkbox"
label for="add_poll" Add a poll label for="add_poll" Add a poll
.toggle-box-container .toggle-box-container
= render PhilomenaWeb.Topic.PollView, "_form.html", Map.put(assigns, :f, fp) = render PhilomenaWeb.Topic.PollView, "_form.html", Map.put(assigns, :f, fp)
.block__tab.communication-edit__tab.hidden data-tab="preview"
' [Loading preview...]
.block__content.communication-edit__actions .block__content.communication-edit__actions
= submit "Post", class: "button", data: [disable_with: raw("Posting&hellip;")] = submit "Post", class: "button", data: [disable_with: raw("Posting&hellip;")]

View file

@ -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&hellip;")] => submit "Post", class: "button", data: [disable_with: raw("Posting&hellip;")]
= 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"

View file

@ -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&hellip;")] => submit "Edit", class: "button", data: [disable_with: raw("Posting&hellip;")]

View file

@ -42,4 +42,4 @@ h1
' Edited ' Edited
=> pretty_time(version.created_at) => pretty_time(version.created_at)
' by ' by
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: version, conn: @conn => render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: version, conn: @conn

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -1,3 +0,0 @@
defmodule PhilomenaWeb.TextileView do
use PhilomenaWeb, :view
end

View file

@ -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

View file

@ -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