mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
revert split editor styling, update markdown toolbar buttons' behavior
This commit is contained in:
parent
b8aa9b2c9c
commit
f61cc6d0be
7 changed files with 90 additions and 156 deletions
|
@ -226,46 +226,3 @@ a.block__header--single-item, .block__header a {
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ span.communication__sender__stats,
|
|||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.communication-edit__wrap {
|
||||
.communication-edit__tab {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ span.communication__sender__stats,
|
|||
}
|
||||
}
|
||||
|
||||
.hyphenate-breaks {
|
||||
.hyphenate-breaks{
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Markdown toolbar
|
||||
*/
|
||||
|
||||
import { $, $$, showEl } from './utils/dom';
|
||||
import { $, $$ } from './utils/dom';
|
||||
|
||||
const markdownSyntax = {
|
||||
bold: {
|
||||
|
@ -22,8 +22,15 @@ const markdownSyntax = {
|
|||
options: { prefix: '||', shortcutKey: 's' }
|
||||
},
|
||||
code: {
|
||||
action: wrapSelection,
|
||||
options: { prefix: '`', shortcutKey: 'e' }
|
||||
action: wrapSelectionOrLines,
|
||||
options: {
|
||||
prefix: '`',
|
||||
suffix: '`',
|
||||
prefixMultiline: '```\n',
|
||||
suffixMultiline: '\n```',
|
||||
singleWrap: true,
|
||||
shortcutKey: 'e'
|
||||
}
|
||||
},
|
||||
strike: {
|
||||
action: wrapSelection,
|
||||
|
@ -62,22 +69,31 @@ function getSelections(textarea, linesOnly = false) {
|
|||
trailingSpace = '',
|
||||
caret;
|
||||
|
||||
if (linesOnly) {
|
||||
const processLinesOnly = linesOnly instanceof RegExp ? linesOnly.test(selection) : linesOnly;
|
||||
if (processLinesOnly) {
|
||||
const explorer = /\n/g;
|
||||
let startNewlineIndex = 0,
|
||||
endNewlineIndex = textarea.value.length;
|
||||
const explorer = /\n/g;
|
||||
while (explorer.exec(textarea.value)) {
|
||||
const { lastIndex } = explorer;
|
||||
if (lastIndex < selectionStart) {
|
||||
startNewlineIndex = lastIndex + 1;
|
||||
if (lastIndex <= selectionStart) {
|
||||
startNewlineIndex = lastIndex;
|
||||
}
|
||||
else if (lastIndex > selectionEnd) {
|
||||
endNewlineIndex = lastIndex;
|
||||
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);
|
||||
}
|
||||
|
@ -98,6 +114,7 @@ function getSelections(textarea, linesOnly = false) {
|
|||
}
|
||||
|
||||
return {
|
||||
processLinesOnly,
|
||||
selectedText: selection,
|
||||
beforeSelection: textarea.value.substring(0, selectionStart) + leadingSpace,
|
||||
afterSelection: trailingSpace + textarea.value.substring(selectionEnd)
|
||||
|
@ -105,11 +122,11 @@ function getSelections(textarea, linesOnly = false) {
|
|||
}
|
||||
|
||||
function transformSelection(textarea, transformer, eachLine) {
|
||||
const { selectedText, beforeSelection, afterSelection } = getSelections(textarea, 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);
|
||||
const { newText, caretOffset } = transformer(selectedText, processLinesOnly);
|
||||
|
||||
textarea.value = beforeSelection + newText + afterSelection;
|
||||
|
||||
|
@ -120,7 +137,8 @@ function transformSelection(textarea, transformer, eachLine) {
|
|||
textarea.selectionStart = newSelectionStart;
|
||||
textarea.selectionEnd = newSelectionStart;
|
||||
textarea.scrollTop = scrollTop;
|
||||
textarea.dispatchEvent(new Event('keydown'));
|
||||
// Needed for automatic textarea resizing
|
||||
textarea.dispatchEvent(new Event('change'));
|
||||
}
|
||||
|
||||
function insertLink(textarea, options) {
|
||||
|
@ -150,28 +168,31 @@ function wrapSelection(textarea, options) {
|
|||
});
|
||||
}
|
||||
|
||||
newText = prefix + newText + suffix
|
||||
|
||||
return {
|
||||
newText: prefix + newText + suffix,
|
||||
caretOffset: emptyText ? prefix.length : 0
|
||||
newText,
|
||||
caretOffset: emptyText ? prefix.length : newText.length
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function wrapLines(textarea, options) {
|
||||
transformSelection(textarea, (selectedText) => {
|
||||
const { text = selectedText, prefix = '', suffix = '' } = options,
|
||||
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 = prefix;
|
||||
let newText = singleWrap
|
||||
? prefix + text.trim() + suffix
|
||||
: text.split(/\n/g).map(line => prefix + line.trim() + suffix).join('\n');
|
||||
|
||||
if (!emptyText) {
|
||||
newText = text.split(/\n/g).map(line => prefix + line.trim() + suffix).join('\n');
|
||||
}
|
||||
else {
|
||||
newText += suffix;
|
||||
}
|
||||
return { newText, caretOffset: emptyText ? prefix.length : newText.length };
|
||||
}, eachLine);
|
||||
}
|
||||
|
||||
return { newText, caretOffset: newText.length };
|
||||
});
|
||||
function wrapSelectionOrLines(textarea, options) {
|
||||
wrapLines(textarea, options, /\n/);
|
||||
}
|
||||
|
||||
function escapeSelection(textarea, options) {
|
||||
|
@ -224,15 +245,6 @@ function setupToolbar() {
|
|||
$$('.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 };
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
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) {
|
||||
|
@ -37,39 +36,21 @@ function commentReply(user, url, textarea, quote) {
|
|||
textarea.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the abort controller for the current preview request
|
||||
* @type {null|AbortController}
|
||||
*/
|
||||
let previewAbortController = null;
|
||||
|
||||
function getPreview(body, anonymous, previewLoading, previewContent) {
|
||||
function getPreview(body, anonymous, previewLoading, previewIdle, previewContent) {
|
||||
const path = '/posts/preview';
|
||||
|
||||
if (typeof body !== 'string') return;
|
||||
|
||||
const trimmedBody = body.trim();
|
||||
if (trimmedBody.length < 1) {
|
||||
previewContent.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
showEl(previewLoading);
|
||||
hideEl(previewIdle);
|
||||
|
||||
// Abort previous requests if it exists
|
||||
if (previewAbortController) previewAbortController.abort();
|
||||
previewAbortController = new AbortController();
|
||||
|
||||
fetchJson('POST', path, { body, anonymous }, previewAbortController.signal)
|
||||
fetchJson('POST', path, { body, anonymous })
|
||||
.then(handleError)
|
||||
.then(data => {
|
||||
previewContent.innerHTML = data;
|
||||
filterNode(previewContent);
|
||||
showEl(previewContent);
|
||||
showEl(previewIdle);
|
||||
hideEl(previewLoading);
|
||||
})
|
||||
.finally(() => {
|
||||
previewAbortController = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -99,34 +80,41 @@ function setupPreviews() {
|
|||
textarea = document.querySelector('.js-preview-description');
|
||||
}
|
||||
|
||||
const previewLoading = document.querySelector('.communication-preview__loading');
|
||||
const previewContent = document.querySelector('.communication-preview__content');
|
||||
const previewButton = document.querySelector('a[data-click-tab="preview"]');
|
||||
const previewLoading = document.querySelector('.js-preview-loading');
|
||||
const previewIdle = document.querySelector('.js-preview-idle');
|
||||
const previewContent = document.querySelector('.js-preview-content');
|
||||
const previewAnon = document.querySelector('.js-preview-anonymous') || false;
|
||||
|
||||
if (!textarea || !previewContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getCacheKey = () => {
|
||||
return (previewAnon && previewAnon.checked ? 'anon;' : '') + textarea.value;
|
||||
}
|
||||
|
||||
const previewedTextAttribute = 'data-previewed-text';
|
||||
const updatePreview = () => {
|
||||
getPreview(textarea.value, previewAnon && previewAnon.checked, previewLoading, previewContent);
|
||||
const cachedValue = getCacheKey()
|
||||
if (previewContent.getAttribute(previewedTextAttribute) === cachedValue) return;
|
||||
previewContent.setAttribute(previewedTextAttribute, cachedValue);
|
||||
|
||||
getPreview(textarea.value, previewAnon && previewAnon.checked, previewLoading, previewIdle, previewContent);
|
||||
};
|
||||
|
||||
const debouncedUpdater = debounce(500, () => {
|
||||
if (previewContent.previewedText === textarea.value) return;
|
||||
previewContent.previewedText = textarea.value;
|
||||
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
textarea.addEventListener('keydown', debouncedUpdater);
|
||||
textarea.addEventListener('focus', debouncedUpdater);
|
||||
previewButton.addEventListener('click', updatePreview);
|
||||
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'));
|
||||
// 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', updatePreview);
|
||||
previewAnon && previewAnon.addEventListener('click', () => {
|
||||
if (previewContent.classList.contains('hidden')) return;
|
||||
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
document.addEventListener('click', event => {
|
||||
if (event.target && event.target.closest('.post-reply')) {
|
||||
|
|
|
@ -22,22 +22,3 @@ 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, signal) {
|
||||
function fetchJson(verb, endpoint, body) {
|
||||
const data = {
|
||||
method: verb,
|
||||
credentials: 'same-origin',
|
||||
|
@ -11,7 +11,6 @@ function fetchJson(verb, endpoint, body, signal) {
|
|||
'x-csrf-token': window.booru.csrfToken,
|
||||
'x-requested-with': 'xmlhttprequest'
|
||||
},
|
||||
signal,
|
||||
};
|
||||
|
||||
if (body) {
|
||||
|
|
|
@ -3,25 +3,22 @@
|
|||
- 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
|
||||
.block__header.block__header--js-tabbed
|
||||
a.selected href="#" data-click-tab="write"
|
||||
i.fa> class="fa-#{action_icon}"
|
||||
= action_text
|
||||
|
||||
= render PhilomenaWeb.MarkdownView, "_help.html", conn: @conn
|
||||
= render PhilomenaWeb.MarkdownView, "_toolbar.html", conn: @conn
|
||||
a href="#" data-click-tab="preview"
|
||||
i.fa.fa-cog.fa-fw.fa-spin.js-preview-loading.hidden> title=raw('Loading preview…')
|
||||
i.fa.fa-eye.fa-fw.js-preview-idle>
|
||||
| Preview
|
||||
|
||||
.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.selected.js-preview-input-wrapper data-tab="write"
|
||||
= render PhilomenaWeb.MarkdownView, "_help.html", conn: @conn
|
||||
= render PhilomenaWeb.MarkdownView, "_toolbar.html", conn: @conn
|
||||
|
||||
.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…')
|
||||
.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
|
||||
|
||||
.communication-preview__content
|
||||
.block__tab.communication-edit__tab.hidden.js-preview-content data-tab="preview"
|
||||
|
|
Loading…
Reference in a new issue