Merge pull request #137 from philomena-dev/markdown-frontend

Markdown-based text editor
This commit is contained in:
Meow 2021-09-30 23:17:21 +02:00 committed by GitHub
commit 77b2fb93bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 424 additions and 482 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,7 @@ a.block__header--single-item, .block__header a {
display: none; display: none;
} }
} }
.block__content--top-border {
border-top: $border;
}

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

@ -0,0 +1,250 @@
/**
* Markdown toolbar
*/
import { $, $$ } from './utils/dom';
const markdownSyntax = {
bold: {
action: wrapSelection,
options: { prefix: '**', shortcutKey: 'b' }
},
italics: {
action: wrapSelection,
options: { prefix: '*', shortcutKey: 'i' }
},
under: {
action: wrapSelection,
options: { prefix: '__', shortcutKey: 'u' }
},
spoiler: {
action: wrapSelection,
options: { prefix: '||', shortcutKey: 's' }
},
code: {
action: wrapSelectionOrLines,
options: {
prefix: '`',
suffix: '`',
prefixMultiline: '```\n',
suffixMultiline: '\n```',
singleWrap: true,
shortcutKey: 'e'
}
},
strike: {
action: wrapSelection,
options: { prefix: '~~' }
},
superscript: {
action: wrapSelection,
options: { prefix: '^' }
},
subscript: {
action: wrapSelection,
options: { prefix: '%' }
},
quote: {
action: wrapLines,
options: { prefix: '> ' }
},
link: {
action: insertLink,
options: { shortcutKey: 'l' }
},
image: {
action: insertLink,
options: { image: true, shortcutKey: 'k' }
},
escape: {
action: escapeSelection,
options: { escapeChar: '\\' }
}
};
function getSelections(textarea, linesOnly = false) {
let { selectionStart, selectionEnd } = textarea,
selection = textarea.value.substring(selectionStart, selectionEnd),
leadingSpace = '',
trailingSpace = '',
caret;
const processLinesOnly = linesOnly instanceof RegExp ? linesOnly.test(selection) : linesOnly;
if (processLinesOnly) {
const explorer = /\n/g;
let startNewlineIndex = 0,
endNewlineIndex = textarea.value.length;
while (explorer.exec(textarea.value)) {
const { lastIndex } = explorer;
if (lastIndex <= selectionStart) {
startNewlineIndex = lastIndex;
}
else if (lastIndex > selectionEnd) {
endNewlineIndex = lastIndex - 1;
break;
}
}
selectionStart = startNewlineIndex;
const startRemovedValue = textarea.value.substring(selectionStart);
const startsWithBlankString = startRemovedValue.match(/^[\s\n]+/);
if (startsWithBlankString) {
// Offset the selection start to the first non-blank line's first non-blank character, since
// Some browsers treat selection up to the start of the line as including the end of the
// previous line
selectionStart += startsWithBlankString[0].length;
}
selectionEnd = endNewlineIndex;
selection = textarea.value.substring(selectionStart, selectionEnd);
}
else {
// Deselect trailing space and line break
for (caret = selection.length - 1; caret > 0; caret--) {
if (selection[caret] !== ' ' && selection[caret] !== '\n') break;
trailingSpace = selection[caret] + trailingSpace;
}
selection = selection.substring(0, caret + 1);
// Deselect leading space and line break
for (caret = 0; caret < selection.length; caret++) {
if (selection[caret] !== ' ' && selection[caret] !== '\n') break;
leadingSpace += selection[caret];
}
selection = selection.substring(caret);
}
return {
processLinesOnly,
selectedText: selection,
beforeSelection: textarea.value.substring(0, selectionStart) + leadingSpace,
afterSelection: trailingSpace + textarea.value.substring(selectionEnd)
};
}
function transformSelection(textarea, transformer, eachLine) {
const { selectedText, beforeSelection, afterSelection, processLinesOnly } = getSelections(textarea, eachLine),
// For long comments, record scrollbar position to restore it later
{ scrollTop } = textarea;
const { newText, caretOffset } = transformer(selectedText, processLinesOnly);
textarea.value = beforeSelection + newText + afterSelection;
const newSelectionStart = caretOffset >= 1
? beforeSelection.length + caretOffset
: textarea.value.length - afterSelection.length - caretOffset;
textarea.selectionStart = newSelectionStart;
textarea.selectionEnd = newSelectionStart;
textarea.scrollTop = scrollTop;
// Needed for automatic textarea resizing
textarea.dispatchEvent(new Event('change'));
}
function insertLink(textarea, options) {
let hyperlink = window.prompt(options.image ? 'Image link:' : 'Link:');
if (!hyperlink || hyperlink === '') return;
// Change on-site link to use relative url
if (!options.image && hyperlink.startsWith(window.location.origin)) {
hyperlink = hyperlink.substring(window.location.origin.length);
}
const prefix = options.image ? '![' : '[',
suffix = `](${hyperlink})`;
wrapSelection(textarea, { prefix, suffix });
}
function wrapSelection(textarea, options) {
transformSelection(textarea, selectedText => {
const { text = selectedText, prefix = '', suffix = options.prefix } = options,
emptyText = text === '';
let newText = text;
if (!emptyText) {
newText = text.replace(/(\n{2,})/g, match => {
return suffix + match + prefix;
});
}
newText = prefix + newText + suffix
return {
newText,
caretOffset: emptyText ? prefix.length : newText.length
};
});
}
function wrapLines(textarea, options, eachLine = true) {
transformSelection(textarea, (selectedText, processLinesOnly) => {
const { text = selectedText, singleWrap = false } = options,
prefix = (processLinesOnly && options.prefixMultiline) || options.prefix || '',
suffix = (processLinesOnly && options.suffixMultiline) || options.suffix || '',
emptyText = text === '';
let newText = singleWrap
? prefix + text.trim() + suffix
: text.split(/\n/g).map(line => prefix + line.trim() + suffix).join('\n');
return { newText, caretOffset: emptyText ? prefix.length : newText.length };
}, eachLine);
}
function wrapSelectionOrLines(textarea, options) {
wrapLines(textarea, options, /\n/);
}
function escapeSelection(textarea, options) {
transformSelection(textarea, selectedText => {
const { text = selectedText } = options,
emptyText = text === '';
if (emptyText) return;
const newText = text.replace(/([*_[\]()^`%\\~<>#|])/g, '\\$1');
return {
newText,
caretOffset: newText.length
};
});
}
function clickHandler(event) {
const button = event.target.closest('.communication__toolbar__button');
if (!button) return;
const toolbar = button.closest('.communication__toolbar'),
// There may be multiple toolbars present on the page,
// in the case of image pages with description edit active
// we target the textarea that shares the same parent as the toolbar
textarea = $('.js-toolbar-input', toolbar.parentNode),
id = button.dataset.syntaxId;
markdownSyntax[id].action(textarea, markdownSyntax[id].options);
textarea.focus();
}
function shortcutHandler(event) {
if (!event.ctrlKey || (window.navigator.platform === 'MacIntel' && !event.metaKey) || event.shiftKey || event.altKey) return;
const textarea = event.target,
key = event.key.toLowerCase();
for (const id in markdownSyntax) {
if (key === markdownSyntax[id].options.shortcutKey) {
markdownSyntax[id].action(textarea, markdownSyntax[id].options);
event.preventDefault();
}
}
}
function setupToolbar() {
$$('.communication__toolbar').forEach(toolbar => {
toolbar.addEventListener('click', clickHandler);
});
$$('.js-toolbar-input').forEach(textarea => {
textarea.addEventListener('keydown', shortcutHandler);
});
}
export { setupToolbar };

View file

@ -1,9 +1,10 @@
/** /**
* Textile previews (posts, comments, messages) * Markdown previews (posts, comments, messages)
*/ */
import { fetchJson } from './utils/requests'; import { fetchJson } from './utils/requests';
import { filterNode } from './imagesclientside'; import { filterNode } from './imagesclientside';
import { hideEl, showEl } from './utils/dom.js';
function handleError(response) { function handleError(response) {
const errorMessage = '<div>Preview failed to load!</div>'; const errorMessage = '<div>Preview failed to load!</div>';
@ -35,43 +36,84 @@ function commentReply(user, url, textarea, quote) {
textarea.focus(); textarea.focus();
} }
function getPreview(body, anonymous, previewTab, isImage = false) { function getPreview(body, anonymous, previewLoading, previewIdle, previewContent) {
let path = '/posts/preview'; const path = '/posts/preview';
if (typeof body !== 'string') return;
showEl(previewLoading);
hideEl(previewIdle);
fetchJson('POST', path, { body, anonymous }) fetchJson('POST', path, { body, anonymous })
.then(handleError) .then(handleError)
.then(data => { .then(data => {
previewTab.innerHTML = data; previewContent.innerHTML = data;
filterNode(previewTab); filterNode(previewContent);
showEl(previewIdle);
hideEl(previewLoading);
}); });
} }
/**
* Resizes the event target <textarea> to match the size of its contained text, between set
* minimum and maximum height values. Former comes from CSS, latter is hard coded below.
* @template {{ target: HTMLTextAreaElement }} E
* @param {E} e
*/
function resizeTextarea(e) {
// Reset inline height for fresh calculations
e.target.style.height = '';
const { borderTopWidth, borderBottomWidth, height } = window.getComputedStyle(e.target);
// Add scrollHeight and borders (because border-box) to get the target size that avoids scrollbars
const contentHeight = e.target.scrollHeight + parseFloat(borderTopWidth) + parseFloat(borderBottomWidth);
// Get the original default height provided by page styles
const regularHeight = parseFloat(height);
// Limit textarea's size to between the original height and 1000px
const newHeight = Math.max(regularHeight, Math.min(1000, contentHeight));
e.target.style.height = `${newHeight}px`;
}
function setupPreviews() { function setupPreviews() {
let textarea = document.querySelector('.js-preview-input'); let textarea = document.querySelector('.js-preview-input');
let imageDesc = false;
if (!textarea) { if (!textarea) {
textarea = document.querySelector('.js-preview-description'); textarea = document.querySelector('.js-preview-description');
imageDesc = true;
} }
const previewButton = document.querySelector('a[data-click-tab="preview"]'); const previewButton = document.querySelector('a[data-click-tab="preview"]');
const previewTab = document.querySelector('.block__tab[data-tab="preview"]'); const previewLoading = document.querySelector('.js-preview-loading');
const previewAnon = document.querySelector('.preview-anonymous') || false; const previewIdle = document.querySelector('.js-preview-idle');
const previewContent = document.querySelector('.js-preview-content');
const previewAnon = document.querySelector('.js-preview-anonymous') || false;
if (!textarea || !previewButton) { if (!textarea || !previewContent) {
return; return;
} }
previewButton.addEventListener('click', () => { const getCacheKey = () => {
if (previewTab.previewedText === textarea.value) return; return (previewAnon && previewAnon.checked ? 'anon;' : '') + textarea.value;
previewTab.previewedText = textarea.value; }
getPreview(textarea.value, Boolean(previewAnon.checked), previewTab, imageDesc); const previewedTextAttribute = 'data-previewed-text';
}); const updatePreview = () => {
const cachedValue = getCacheKey()
if (previewContent.getAttribute(previewedTextAttribute) === cachedValue) return;
previewContent.setAttribute(previewedTextAttribute, cachedValue);
getPreview(textarea.value, previewAnon && previewAnon.checked, previewLoading, previewIdle, previewContent);
};
previewButton.addEventListener('click', updatePreview);
textarea.addEventListener('change', resizeTextarea);
textarea.addEventListener('keyup', resizeTextarea);
// Fire handler for automatic resizing if textarea contains text on page load (e.g. editing)
if (textarea.value) textarea.dispatchEvent(new Event('change'));
previewAnon && previewAnon.addEventListener('click', () => { previewAnon && previewAnon.addEventListener('click', () => {
getPreview(textarea.value, Boolean(previewAnon.checked), previewTab, imageDesc); if (previewContent.classList.contains('hidden')) return;
updatePreview();
}); });
document.addEventListener('click', event => { document.addEventListener('click', event => {

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

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

@ -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>
' Preview
.block__tab.communication-edit__tab.selected data-tab="write"
= render PhilomenaWeb.TextileView, "_help.html", conn: @conn
= render PhilomenaWeb.TextileView, "_toolbar.html", conn: @conn
.field
= textarea f, :body, class: "input input--wide input--text js-preview-input js-toolbar-input", placeholder: "Please read the site rules before posting and use [spoiler][/spoiler] for above-rating stuff.", required: true
= error_tag f, :body
.field
= text_input f, :edit_reason, class: "input input--wide", placeholder: "Reason for edit" = text_input f, :edit_reason, class: "input input--wide", placeholder: "Reason for edit"
= error_tag f, :edit_reason = error_tag f, :edit_reason
.block__tab.communication-edit__tab.hidden data-tab="preview"
' [Loading preview...]
.block__content.communication-edit__actions .block__content.communication-edit__actions
=> submit "Edit", class: "button", data: [disable_with: raw("Posting&hellip;")] => submit "Edit", class: "button", data: [disable_with: raw("Posting&hellip;")]

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

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

@ -0,0 +1,17 @@
.editor-syntax-reference
strong<> Syntax quick reference:
span
strong> **bold**
em> *italic*
span.spoiler> ||hide text||
code> `code`
ins> __underline__
del> ~~strike~~
sup> ^sup^
sub %sub%
p
a href="/pages/markdown"
i.fa.fa-question-circle>
strong Detailed syntax guide

View file

@ -0,0 +1,24 @@
- form = assigns[:f]
- action_text = assigns[:action_text] || 'Edit'
- action_icon = assigns[:action_icon] || 'edit'
- field_name = assigns[:name] || :body
- field_placeholder = assigns[:placeholder] || "Your message"
.block__header.block__header--js-tabbed
a.selected href="#" data-click-tab="write"
i.fa> class="fa-#{action_icon}"
= action_text
a href="#" data-click-tab="preview"
i.fa.fa-cog.fa-fw.fa-spin.js-preview-loading.hidden> title=raw('Loading preview&hellip;')
i.fa.fa-eye.fa-fw.js-preview-idle>
| Preview
.block__tab.communication-edit__tab.selected.js-preview-input-wrapper data-tab="write"
= render PhilomenaWeb.MarkdownView, "_help.html", conn: @conn
= render PhilomenaWeb.MarkdownView, "_toolbar.html", conn: @conn
.field
= textarea form, field_name, class: "input input--wide input--text input--resize-vertical js-toolbar-input js-preview-input", placeholder: field_placeholder, required: true
= error_tag form, field_name
.block__tab.communication-edit__tab.hidden.js-preview-content data-tab="preview"

View file

@ -29,6 +29,6 @@
i.fa.fa-link i.fa.fa-link
button.communication__toolbar__button tabindex="-1" type="button" title="insert image (ctrl+k)" data-syntax-id="image" button.communication__toolbar__button tabindex="-1" type="button" title="insert image (ctrl+k)" data-syntax-id="image"
i.fa.fa-image i.fa.fa-image
button.communication__toolbar__button tabindex="-1" type="button" title="Text you want the parser to ignore" data-syntax-id="noParse" button.communication__toolbar__button tabindex="-1" type="button" title="Text you want the parser to ignore" data-syntax-id="escape"
span span
| no parse | escape

View file

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

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

@ -1,35 +0,0 @@
.textile-syntax-reference
strong<> Syntax quick reference:
span
strong>
' *bold*
em> _italic_
span.spoiler>
| [spoiler]hide text[/spoiler]
code> @code@
ins> +underline+
del> -strike-
sup> ^sup^
sub ~sub~
button< class="button" type="button" data-click-toggle="button:hover + .textile_help, button:focus + .textile_help"
' &hellip;
p.hidden.textile_help
' [==stuff you don't want textile to parse==]
br
ins> Links:
' "On-site link":/some-link, "External link":http://some-link
br
ins> Images:
' &gt;&gt;1 — link to image, &gt;&gt;1t — embed image thumbnail (&gt;&gt;1p — large preview, &gt;&gt;1s &mdash; small thumbnail)
br
ins> External images:
' !http://some-image!, !http://some-clickable-image!:http://some-link
br
strong> Remember to use embeds (&gt;&gt;) for booru images as these let users filter content they don't want to see

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

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