forgejo/web_src/js/utils/dom.js
wxiaoguang 93eb914438
Improve markdown editor: width, height, preferred (#23895)
Follow #23876


1. Fine tune the heights of the editors (like before)
    * Auto expand the editor (increase/decrease the height) when editing
2. Remember user's last used editor (textarea/easymde) in LocalStorage,
then next time the editor will be switched automatically
* No need to introduce extra config option, it satisfies all users,
including who prefer EasyMDE
3. Also fix the width problem of Review Panel

Screenshot:

<details>


![image](https://user-images.githubusercontent.com/2114189/229518585-2e05827e-8355-48f3-a20c-2c8b9e60ce74.png)


![image](https://user-images.githubusercontent.com/2114189/229518173-4caa6da7-6ad9-40e9-bf1a-ceddfcd4b37f.png)


![image](https://user-images.githubusercontent.com/2114189/229507886-148e9b84-9b58-46d1-ba3f-727e1396f476.png)


![image](https://user-images.githubusercontent.com/2114189/229518258-9f522294-1e64-4b06-91ab-ab43b0353aaa.png)


![image](https://user-images.githubusercontent.com/2114189/229507752-6d540ac7-7748-4bb6-bc09-28acab32d31b.png)


![image](https://user-images.githubusercontent.com/2114189/229510899-de322af5-57e8-4dc5-9a61-771a3b1bee79.png)


</details>

---------

Co-authored-by: silverwind <me@silverwind.io>
2023-04-07 13:03:29 -04:00

173 lines
5.7 KiB
JavaScript

function elementsCall(el, func, ...args) {
if (typeof el === 'string' || el instanceof String) {
el = document.querySelectorAll(el);
}
if (el instanceof Node) {
func(el, ...args);
} else if (el.length !== undefined) {
// this works for: NodeList, HTMLCollection, Array, jQuery
for (const e of el) {
func(e, ...args);
}
} else {
throw new Error('invalid argument to be shown/hidden');
}
}
/**
* @param el string (selector), Node, NodeList, HTMLCollection, Array or jQuery
* @param force force=true to show or force=false to hide, undefined to toggle
*/
function toggleShown(el, force) {
if (force === true) {
el.classList.remove('gt-hidden');
} else if (force === false) {
el.classList.add('gt-hidden');
} else if (force === undefined) {
el.classList.toggle('gt-hidden');
} else {
throw new Error('invalid force argument');
}
}
export function showElem(el) {
elementsCall(el, toggleShown, true);
}
export function hideElem(el) {
elementsCall(el, toggleShown, false);
}
export function toggleElem(el, force) {
elementsCall(el, toggleShown, force);
}
export function onDomReady(cb) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', cb);
} else {
cb();
}
}
// autosize a textarea to fit content. Based on
// https://github.com/github/textarea-autosize
// ---------------------------------------------------------------------
// Copyright (c) 2018 GitHub, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// ---------------------------------------------------------------------
export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
let isUserResized = false;
// lastStyleHeight and initialStyleHeight are CSS values like '100px'
let lastMouseX, lastMouseY, lastStyleHeight, initialStyleHeight;
function onUserResize(event) {
if (isUserResized) return;
if (lastMouseX !== event.clientX || lastMouseY !== event.clientY) {
const newStyleHeight = textarea.style.height;
if (lastStyleHeight && lastStyleHeight !== newStyleHeight) {
isUserResized = true;
}
lastStyleHeight = newStyleHeight;
}
lastMouseX = event.clientX;
lastMouseY = event.clientY;
}
function overflowOffset() {
let offsetTop = 0;
let el = textarea;
while (el !== document.body && el !== null) {
offsetTop += el.offsetTop || 0;
el = el.offsetParent;
}
const top = offsetTop - document.defaultView.scrollY;
const bottom = document.documentElement.clientHeight - (top + textarea.offsetHeight);
return {top, bottom};
}
function resizeToFit() {
if (isUserResized) return;
if (textarea.offsetWidth <= 0 && textarea.offsetHeight <= 0) return;
try {
const {top, bottom} = overflowOffset();
const isOutOfViewport = top < 0 || bottom < 0;
const computedStyle = getComputedStyle(textarea);
const topBorderWidth = parseFloat(computedStyle.borderTopWidth);
const bottomBorderWidth = parseFloat(computedStyle.borderBottomWidth);
const isBorderBox = computedStyle.boxSizing === 'border-box';
const borderAddOn = isBorderBox ? topBorderWidth + bottomBorderWidth : 0;
const adjustedViewportMarginBottom = bottom < viewportMarginBottom ? bottom : viewportMarginBottom;
const curHeight = parseFloat(computedStyle.height);
const maxHeight = curHeight + bottom - adjustedViewportMarginBottom;
textarea.style.height = 'auto';
let newHeight = textarea.scrollHeight + borderAddOn;
if (isOutOfViewport) {
// it is already out of the viewport:
// * if the textarea is expanding: do not resize it
if (newHeight > curHeight) {
newHeight = curHeight;
}
// * if the textarea is shrinking, shrink line by line (just use the
// scrollHeight). do not apply max-height limit, otherwise the page
// flickers and the textarea jumps
} else {
// * if it is in the viewport, apply the max-height limit
newHeight = Math.min(maxHeight, newHeight);
}
textarea.style.height = `${newHeight}px`;
lastStyleHeight = textarea.style.height;
} finally {
// ensure that the textarea is fully scrolled to the end, when the cursor
// is at the end during an input event
if (textarea.selectionStart === textarea.selectionEnd &&
textarea.selectionStart === textarea.value.length) {
textarea.scrollTop = textarea.scrollHeight;
}
}
}
function onFormReset() {
isUserResized = false;
if (initialStyleHeight !== undefined) {
textarea.style.height = initialStyleHeight;
} else {
textarea.style.removeProperty('height');
}
}
textarea.addEventListener('mousemove', onUserResize);
textarea.addEventListener('input', resizeToFit);
textarea.form?.addEventListener('reset', onFormReset);
initialStyleHeight = textarea.style.height ?? undefined;
if (textarea.value) resizeToFit();
return {
resizeToFit,
destroy() {
textarea.removeEventListener('mousemove', onUserResize);
textarea.removeEventListener('input', resizeToFit);
textarea.form?.removeEventListener('reset', onFormReset);
}
};
}