revert split editor styling, update markdown toolbar buttons' behavior

This commit is contained in:
SeinopSys 2021-09-29 20:00:46 +02:00
parent b8aa9b2c9c
commit f61cc6d0be
No known key found for this signature in database
GPG key ID: 9BFB053C1BA6C5C4
7 changed files with 90 additions and 156 deletions

View file

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

View file

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

View file

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

View file

@ -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')) {

View file

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

View file

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

View file

@ -3,12 +3,17 @@
- 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
.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
@ -16,12 +21,4 @@
= 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
.block__tab.communication-edit__tab.hidden.js-preview-content data-tab="preview"