mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-10 09:09:02 +00:00
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>
This commit is contained in:
parent
97d5ec2aeb
commit
93eb914438
|
@ -3,14 +3,17 @@
|
||||||
{{if not $textareaContent}}{{$textareaContent = .PullRequestTemplate}}{{end}}
|
{{if not $textareaContent}}{{$textareaContent = .PullRequestTemplate}}{{end}}
|
||||||
{{if not $textareaContent}}{{$textareaContent = .content}}{{end}}
|
{{if not $textareaContent}}{{$textareaContent = .content}}{{end}}
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
{{template "shared/combomarkdowneditor" (dict
|
{{template "shared/combomarkdowneditor" (dict
|
||||||
"locale" $.locale
|
"locale" $.locale
|
||||||
"MarkdownPreviewUrl" (print .Repository.Link "/markup")
|
"MarkdownPreviewUrl" (print .Repository.Link "/markup")
|
||||||
"MarkdownPreviewContext" .RepoLink
|
"MarkdownPreviewContext" .RepoLink
|
||||||
"TextareaName" "content"
|
"TextareaName" "content"
|
||||||
"TextareaContent" $textareaContent
|
"TextareaContent" $textareaContent
|
||||||
|
"TextareaPlaceholder" ($.locale.Tr "repo.diff.comment.placeholder")
|
||||||
"DropzoneParentContainer" "form, .ui.form"
|
"DropzoneParentContainer" "form, .ui.form"
|
||||||
)}}
|
)}}
|
||||||
|
</div>
|
||||||
|
|
||||||
{{if .IsAttachmentEnabled}}
|
{{if .IsAttachmentEnabled}}
|
||||||
<div class="field">
|
<div class="field">
|
||||||
|
|
|
@ -18,8 +18,15 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.combo-markdown-editor .markdown-text-editor {
|
.ui.form .combo-markdown-editor textarea.markdown-text-editor,
|
||||||
|
.combo-markdown-editor textarea.markdown-text-editor {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
min-height: 200px;
|
||||||
|
max-height: calc(100vh - 200px);
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.combo-markdown-editor .CodeMirror-scroll {
|
||||||
|
max-height: calc(100vh - 200px);
|
||||||
}
|
}
|
||||||
|
|
|
@ -544,10 +544,6 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository .comment textarea {
|
|
||||||
max-height: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository.new.issue .comment.form .comment .avatar {
|
.repository.new.issue .comment.form .comment .avatar {
|
||||||
width: 3em;
|
width: 3em;
|
||||||
}
|
}
|
||||||
|
@ -1068,11 +1064,6 @@
|
||||||
min-height: 5rem;
|
min-height: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.view.issue .comment-list .comment .ui.form textarea {
|
|
||||||
height: 200px;
|
|
||||||
font-family: var(--fonts-monospace);
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository.view.issue .comment-list .comment .edit.buttons {
|
.repository.view.issue .comment-list .comment .edit.buttons {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
@ -1191,15 +1182,6 @@
|
||||||
margin-top: -8px;
|
margin-top: -8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository .comment.form .content textarea {
|
|
||||||
height: 200px;
|
|
||||||
font-family: var(--fonts-monospace);
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository .comment.form .content .CodeMirror-scroll {
|
|
||||||
max-height: 85vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.repository .milestone.list {
|
.repository .milestone.list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
|
@ -2123,9 +2105,6 @@
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repository.wiki .form .CodeMirror-scroll {
|
|
||||||
max-height: 85vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.repository.wiki .dividing.header .stackable.grid .button {
|
.repository.wiki .dividing.header .stackable.grid .button {
|
||||||
|
|
|
@ -154,8 +154,11 @@
|
||||||
margin: 0.5em;
|
margin: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-code-cloud .editor-statusbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.comment-code-cloud .footer {
|
.comment-code-cloud .footer {
|
||||||
border-top: 1px solid var(--color-secondary);
|
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,15 +221,9 @@ a.blob-excerpt:hover {
|
||||||
max-height: calc(100vh - 360px);
|
max-height: calc(100vh - 360px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-box-panel .editor-toolbar,
|
.review-box-panel .combo-markdown-editor {
|
||||||
.review-box-panel .CodeMirror-scroll {
|
width: 730px; /* this width matches current EasyMDE's toolbar's width */
|
||||||
width: min(calc(100vw - 2em), 800px);
|
max-width: calc(100vw - 70px); /* leave enough space on left, and align the page content */
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.review-box-panel .combo-markdown-editor textarea {
|
|
||||||
width: 730px;
|
|
||||||
max-width: calc(100vw - 70px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#review-box {
|
#review-box {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import '@github/markdown-toolbar-element';
|
import '@github/markdown-toolbar-element';
|
||||||
import {attachTribute} from '../tribute.js';
|
|
||||||
import {hideElem, showElem} from '../../utils/dom.js';
|
|
||||||
import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js';
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
import {attachTribute} from '../tribute.js';
|
||||||
|
import {hideElem, showElem, autosize} from '../../utils/dom.js';
|
||||||
|
import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js';
|
||||||
import {initMarkupContent} from '../../markup/content.js';
|
import {initMarkupContent} from '../../markup/content.js';
|
||||||
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
|
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
|
||||||
import {attachRefIssueContextPopup} from '../contextpopup.js';
|
import {attachRefIssueContextPopup} from '../contextpopup.js';
|
||||||
|
@ -39,31 +39,55 @@ class ComboMarkdownEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
this.prepareEasyMDEToolbarActions();
|
||||||
|
|
||||||
|
this.setupTab();
|
||||||
|
this.setupDropzone();
|
||||||
|
|
||||||
|
this.setupTextarea();
|
||||||
|
|
||||||
|
await attachTribute(this.textarea, {mentions: true, emoji: true});
|
||||||
|
|
||||||
|
if (this.userPreferredEditor === 'easymde') {
|
||||||
|
await this.switchToEasyMDE();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyEditorHeights(el, heights) {
|
||||||
|
if (!heights) return;
|
||||||
|
if (heights.minHeight) el.style.minHeight = heights.minHeight;
|
||||||
|
if (heights.height) el.style.height = heights.height;
|
||||||
|
if (heights.maxHeight) el.style.maxHeight = heights.maxHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupTextarea() {
|
||||||
this.textarea = this.container.querySelector('.markdown-text-editor');
|
this.textarea = this.container.querySelector('.markdown-text-editor');
|
||||||
this.textarea._giteaComboMarkdownEditor = this;
|
this.textarea._giteaComboMarkdownEditor = this;
|
||||||
this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter)}`;
|
this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter++)}`;
|
||||||
this.textarea.addEventListener('input', (e) => {this.options?.onContentChanged?.(this, e)});
|
this.textarea.addEventListener('input', (e) => this.options?.onContentChanged?.(this, e));
|
||||||
|
this.applyEditorHeights(this.textarea, this.options.editorHeights);
|
||||||
|
this.textareaAutosize = autosize(this.textarea, {viewportMarginBottom: 130});
|
||||||
|
|
||||||
this.textareaMarkdownToolbar = this.container.querySelector('markdown-toolbar');
|
this.textareaMarkdownToolbar = this.container.querySelector('markdown-toolbar');
|
||||||
this.textareaMarkdownToolbar.setAttribute('for', this.textarea.id);
|
this.textareaMarkdownToolbar.setAttribute('for', this.textarea.id);
|
||||||
|
|
||||||
elementIdCounter++;
|
|
||||||
|
|
||||||
this.switchToEasyMDEButton = this.container.querySelector('.markdown-switch-easymde');
|
this.switchToEasyMDEButton = this.container.querySelector('.markdown-switch-easymde');
|
||||||
this.switchToEasyMDEButton?.addEventListener('click', async (e) => {
|
this.switchToEasyMDEButton?.addEventListener('click', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
this.userPreferredEditor = 'easymde';
|
||||||
await this.switchToEasyMDE();
|
await this.switchToEasyMDE();
|
||||||
});
|
});
|
||||||
|
|
||||||
await attachTribute(this.textarea, {mentions: true, emoji: true});
|
if (this.dropzone) {
|
||||||
|
initTextareaImagePaste(this.textarea, this.dropzone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupDropzone() {
|
||||||
const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container');
|
const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container');
|
||||||
if (dropzoneParentContainer) {
|
if (dropzoneParentContainer) {
|
||||||
this.dropzone = this.container.closest(this.container.getAttribute('data-dropzone-parent-container'))?.querySelector('.dropzone');
|
this.dropzone = this.container.closest(this.container.getAttribute('data-dropzone-parent-container'))?.querySelector('.dropzone');
|
||||||
initTextareaImagePaste(this.textarea, this.dropzone);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setupTab();
|
|
||||||
this.prepareEasyMDEToolbarActions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupTab() {
|
setupTab() {
|
||||||
|
@ -134,7 +158,10 @@ class ComboMarkdownEditor {
|
||||||
title: 'Add Checkbox (checked)',
|
title: 'Add Checkbox (checked)',
|
||||||
},
|
},
|
||||||
'gitea-switch-to-textarea': {
|
'gitea-switch-to-textarea': {
|
||||||
action: this.switchToTextarea.bind(this),
|
action: () => {
|
||||||
|
this.userPreferredEditor = 'textarea';
|
||||||
|
this.switchToTextarea();
|
||||||
|
},
|
||||||
className: 'fa fa-file',
|
className: 'fa fa-file',
|
||||||
title: 'Revert to simple textarea',
|
title: 'Revert to simple textarea',
|
||||||
},
|
},
|
||||||
|
@ -169,7 +196,7 @@ class ComboMarkdownEditor {
|
||||||
return processed;
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async switchToTextarea() {
|
switchToTextarea() {
|
||||||
showElem(this.textareaMarkdownToolbar);
|
showElem(this.textareaMarkdownToolbar);
|
||||||
if (this.easyMDE) {
|
if (this.easyMDE) {
|
||||||
this.easyMDE.toTextArea();
|
this.easyMDE.toTextArea();
|
||||||
|
@ -218,6 +245,7 @@ class ComboMarkdownEditor {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this.applyEditorHeights(this.container.querySelector('.CodeMirror-scroll'), this.options.editorHeights);
|
||||||
await attachTribute(this.easyMDE.codemirror.getInputField(), {mentions: true, emoji: true});
|
await attachTribute(this.easyMDE.codemirror.getInputField(), {mentions: true, emoji: true});
|
||||||
initEasyMDEImagePaste(this.easyMDE, this.dropzone);
|
initEasyMDEImagePaste(this.easyMDE, this.dropzone);
|
||||||
hideElem(this.textareaMarkdownToolbar);
|
hideElem(this.textareaMarkdownToolbar);
|
||||||
|
@ -236,6 +264,7 @@ class ComboMarkdownEditor {
|
||||||
} else {
|
} else {
|
||||||
this.textarea.value = v;
|
this.textarea.value = v;
|
||||||
}
|
}
|
||||||
|
this.textareaAutosize.resizeToFit();
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
|
@ -254,6 +283,13 @@ class ComboMarkdownEditor {
|
||||||
this.easyMDE.codemirror.setCursor(this.easyMDE.codemirror.lineCount(), 0);
|
this.easyMDE.codemirror.setCursor(this.easyMDE.codemirror.lineCount(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get userPreferredEditor() {
|
||||||
|
return window.localStorage.getItem(`markdown-editor-${this.options.useScene ?? 'default'}`);
|
||||||
|
}
|
||||||
|
set userPreferredEditor(s) {
|
||||||
|
window.localStorage.setItem(`markdown-editor-${this.options.useScene ?? 'default'}`, s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getComboMarkdownEditor(el) {
|
export function getComboMarkdownEditor(el) {
|
||||||
|
|
|
@ -44,6 +44,11 @@ async function initRepoWikiFormEditor() {
|
||||||
renderEasyMDEPreview();
|
renderEasyMDEPreview();
|
||||||
|
|
||||||
editor = await initComboMarkdownEditor($editorContainer, {
|
editor = await initComboMarkdownEditor($editorContainer, {
|
||||||
|
useScene: 'wiki',
|
||||||
|
// EasyMDE has some problems of height definition, it has inline style height 300px by default, so we also use inline styles to override it.
|
||||||
|
// And another benefit is that we only need to write the style once for both editors.
|
||||||
|
// TODO: Move height style to CSS after EasyMDE removal.
|
||||||
|
editorHeights: {minHeight: '300px', height: 'calc(100vh - 600px)'},
|
||||||
previewMode: 'gfm',
|
previewMode: 'gfm',
|
||||||
previewWiki: true,
|
previewWiki: true,
|
||||||
easyMDEOptions: {
|
easyMDEOptions: {
|
||||||
|
|
|
@ -49,3 +49,124 @@ export function onDomReady(cb) {
|
||||||
cb();
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue