philomena/assets/js/preview.js

126 lines
4.1 KiB
JavaScript

/**
* Markdown previews (posts, comments, messages)
*/
import { fetchJson } from './utils/requests';
import { filterNode } from './imagesclientside';
import { hideEl, showEl } from './utils/dom.js';
function handleError(response) {
const errorMessage = '<div>Preview failed to load!</div>';
if (!response.ok) {
return errorMessage;
}
return response.text();
}
function commentReply(user, url, textarea, quote) {
const text = `"@${user}":${url}`;
let newval = textarea.value;
if (newval && /\n$/.test(newval)) newval += '\n';
newval += `${text}\n`;
if (quote) {
newval += `[bq="${user.replace('"', '\'')}"] ${quote} [/bq]\n`;
}
textarea.value = newval;
textarea.selectionStart = textarea.selectionEnd = newval.length;
const writeTabToggle = document.querySelector('a[data-click-tab="write"]:not(.selected)');
if (writeTabToggle) writeTabToggle.click();
textarea.focus();
}
function getPreview(body, anonymous, previewLoading, previewIdle, previewContent) {
const path = '/posts/preview';
if (typeof body !== 'string') return;
showEl(previewLoading);
hideEl(previewIdle);
fetchJson('POST', path, { body, anonymous })
.then(handleError)
.then(data => {
previewContent.innerHTML = data;
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) {
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 currentHeight = parseFloat(height);
// Limit textarea's size to between the original height and 1000px
const newHeight = Math.max(currentHeight, Math.min(1000, contentHeight));
e.target.style.height = `${newHeight}px`;
}
function setupPreviews() {
let textarea = document.querySelector('.js-preview-input');
if (!textarea) {
textarea = document.querySelector('.js-preview-description');
}
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 = () => {
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', () => {
if (previewContent.classList.contains('hidden')) return;
updatePreview();
});
document.addEventListener('click', event => {
if (event.target && event.target.closest('.post-reply')) {
const link = event.target.closest('.post-reply');
commentReply(link.dataset.author, link.getAttribute('href'), textarea, link.dataset.post);
event.preventDefault();
}
});
}
export { setupPreviews };