Skip to content

Commit 93eb914

Browse files
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 <[email protected]>
1 parent 97d5ec2 commit 93eb914

File tree

7 files changed

+202
-54
lines changed

7 files changed

+202
-54
lines changed

templates/repo/issue/comment_tab.tmpl

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
{{if not $textareaContent}}{{$textareaContent = .PullRequestTemplate}}{{end}}
44
{{if not $textareaContent}}{{$textareaContent = .content}}{{end}}
55

6-
{{template "shared/combomarkdowneditor" (dict
7-
"locale" $.locale
8-
"MarkdownPreviewUrl" (print .Repository.Link "/markup")
9-
"MarkdownPreviewContext" .RepoLink
10-
"TextareaName" "content"
11-
"TextareaContent" $textareaContent
12-
"DropzoneParentContainer" "form, .ui.form"
13-
)}}
6+
<div class="field">
7+
{{template "shared/combomarkdowneditor" (dict
8+
"locale" $.locale
9+
"MarkdownPreviewUrl" (print .Repository.Link "/markup")
10+
"MarkdownPreviewContext" .RepoLink
11+
"TextareaName" "content"
12+
"TextareaContent" $textareaContent
13+
"TextareaPlaceholder" ($.locale.Tr "repo.diff.comment.placeholder")
14+
"DropzoneParentContainer" "form, .ui.form"
15+
)}}
16+
</div>
1417

1518
{{if .IsAttachmentEnabled}}
1619
<div class="field">

web_src/css/editor-markdown.css

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,15 @@
1818
cursor: pointer;
1919
}
2020

21-
.combo-markdown-editor .markdown-text-editor {
21+
.ui.form .combo-markdown-editor textarea.markdown-text-editor,
22+
.combo-markdown-editor textarea.markdown-text-editor {
2223
display: block;
2324
width: 100%;
24-
height: 200px;
25+
min-height: 200px;
26+
max-height: calc(100vh - 200px);
27+
resize: vertical;
28+
}
29+
30+
.combo-markdown-editor .CodeMirror-scroll {
31+
max-height: calc(100vh - 200px);
2532
}

web_src/css/repository.css

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -544,10 +544,6 @@
544544
margin: 0;
545545
}
546546

547-
.repository .comment textarea {
548-
max-height: none !important;
549-
}
550-
551547
.repository.new.issue .comment.form .comment .avatar {
552548
width: 3em;
553549
}
@@ -1068,11 +1064,6 @@
10681064
min-height: 5rem;
10691065
}
10701066

1071-
.repository.view.issue .comment-list .comment .ui.form textarea {
1072-
height: 200px;
1073-
font-family: var(--fonts-monospace);
1074-
}
1075-
10761067
.repository.view.issue .comment-list .comment .edit.buttons {
10771068
margin-top: 10px;
10781069
}
@@ -1191,15 +1182,6 @@
11911182
margin-top: -8px;
11921183
}
11931184

1194-
.repository .comment.form .content textarea {
1195-
height: 200px;
1196-
font-family: var(--fonts-monospace);
1197-
}
1198-
1199-
.repository .comment.form .content .CodeMirror-scroll {
1200-
max-height: 85vh;
1201-
}
1202-
12031185
.repository .milestone.list {
12041186
list-style: none;
12051187
padding-top: 15px;
@@ -2123,9 +2105,6 @@
21232105
margin-top: 0;
21242106
}
21252107

2126-
.repository.wiki .form .CodeMirror-scroll {
2127-
max-height: 85vh;
2128-
}
21292108

21302109
@media (max-width: 767px) {
21312110
.repository.wiki .dividing.header .stackable.grid .button {

web_src/css/review.css

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,11 @@
154154
margin: 0.5em;
155155
}
156156

157+
.comment-code-cloud .editor-statusbar {
158+
display: none;
159+
}
160+
157161
.comment-code-cloud .footer {
158-
border-top: 1px solid var(--color-secondary);
159162
padding: 10px 0;
160163
}
161164

@@ -218,15 +221,9 @@ a.blob-excerpt:hover {
218221
max-height: calc(100vh - 360px);
219222
}
220223

221-
.review-box-panel .editor-toolbar,
222-
.review-box-panel .CodeMirror-scroll {
223-
width: min(calc(100vw - 2em), 800px);
224-
max-width: none;
225-
}
226-
227-
.review-box-panel .combo-markdown-editor textarea {
228-
width: 730px;
229-
max-width: calc(100vw - 70px);
224+
.review-box-panel .combo-markdown-editor {
225+
width: 730px; /* this width matches current EasyMDE's toolbar's width */
226+
max-width: calc(100vw - 70px); /* leave enough space on left, and align the page content */
230227
}
231228

232229
#review-box {

web_src/js/features/comp/ComboMarkdownEditor.js

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import '@github/markdown-toolbar-element';
2+
import $ from 'jquery';
23
import {attachTribute} from '../tribute.js';
3-
import {hideElem, showElem} from '../../utils/dom.js';
4+
import {hideElem, showElem, autosize} from '../../utils/dom.js';
45
import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js';
5-
import $ from 'jquery';
66
import {initMarkupContent} from '../../markup/content.js';
77
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
88
import {attachRefIssueContextPopup} from '../contextpopup.js';
@@ -39,31 +39,55 @@ class ComboMarkdownEditor {
3939
}
4040

4141
async init() {
42+
this.prepareEasyMDEToolbarActions();
43+
44+
this.setupTab();
45+
this.setupDropzone();
46+
47+
this.setupTextarea();
48+
49+
await attachTribute(this.textarea, {mentions: true, emoji: true});
50+
51+
if (this.userPreferredEditor === 'easymde') {
52+
await this.switchToEasyMDE();
53+
}
54+
}
55+
56+
applyEditorHeights(el, heights) {
57+
if (!heights) return;
58+
if (heights.minHeight) el.style.minHeight = heights.minHeight;
59+
if (heights.height) el.style.height = heights.height;
60+
if (heights.maxHeight) el.style.maxHeight = heights.maxHeight;
61+
}
62+
63+
setupTextarea() {
4264
this.textarea = this.container.querySelector('.markdown-text-editor');
4365
this.textarea._giteaComboMarkdownEditor = this;
44-
this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter)}`;
45-
this.textarea.addEventListener('input', (e) => {this.options?.onContentChanged?.(this, e)});
66+
this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter++)}`;
67+
this.textarea.addEventListener('input', (e) => this.options?.onContentChanged?.(this, e));
68+
this.applyEditorHeights(this.textarea, this.options.editorHeights);
69+
this.textareaAutosize = autosize(this.textarea, {viewportMarginBottom: 130});
70+
4671
this.textareaMarkdownToolbar = this.container.querySelector('markdown-toolbar');
4772
this.textareaMarkdownToolbar.setAttribute('for', this.textarea.id);
4873

49-
elementIdCounter++;
50-
5174
this.switchToEasyMDEButton = this.container.querySelector('.markdown-switch-easymde');
5275
this.switchToEasyMDEButton?.addEventListener('click', async (e) => {
5376
e.preventDefault();
77+
this.userPreferredEditor = 'easymde';
5478
await this.switchToEasyMDE();
5579
});
5680

57-
await attachTribute(this.textarea, {mentions: true, emoji: true});
81+
if (this.dropzone) {
82+
initTextareaImagePaste(this.textarea, this.dropzone);
83+
}
84+
}
5885

86+
setupDropzone() {
5987
const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container');
6088
if (dropzoneParentContainer) {
6189
this.dropzone = this.container.closest(this.container.getAttribute('data-dropzone-parent-container'))?.querySelector('.dropzone');
62-
initTextareaImagePaste(this.textarea, this.dropzone);
6390
}
64-
65-
this.setupTab();
66-
this.prepareEasyMDEToolbarActions();
6791
}
6892

6993
setupTab() {
@@ -134,7 +158,10 @@ class ComboMarkdownEditor {
134158
title: 'Add Checkbox (checked)',
135159
},
136160
'gitea-switch-to-textarea': {
137-
action: this.switchToTextarea.bind(this),
161+
action: () => {
162+
this.userPreferredEditor = 'textarea';
163+
this.switchToTextarea();
164+
},
138165
className: 'fa fa-file',
139166
title: 'Revert to simple textarea',
140167
},
@@ -169,7 +196,7 @@ class ComboMarkdownEditor {
169196
return processed;
170197
}
171198

172-
async switchToTextarea() {
199+
switchToTextarea() {
173200
showElem(this.textareaMarkdownToolbar);
174201
if (this.easyMDE) {
175202
this.easyMDE.toTextArea();
@@ -218,6 +245,7 @@ class ComboMarkdownEditor {
218245
}
219246
},
220247
});
248+
this.applyEditorHeights(this.container.querySelector('.CodeMirror-scroll'), this.options.editorHeights);
221249
await attachTribute(this.easyMDE.codemirror.getInputField(), {mentions: true, emoji: true});
222250
initEasyMDEImagePaste(this.easyMDE, this.dropzone);
223251
hideElem(this.textareaMarkdownToolbar);
@@ -236,6 +264,7 @@ class ComboMarkdownEditor {
236264
} else {
237265
this.textarea.value = v;
238266
}
267+
this.textareaAutosize.resizeToFit();
239268
}
240269

241270
focus() {
@@ -254,6 +283,13 @@ class ComboMarkdownEditor {
254283
this.easyMDE.codemirror.setCursor(this.easyMDE.codemirror.lineCount(), 0);
255284
}
256285
}
286+
287+
get userPreferredEditor() {
288+
return window.localStorage.getItem(`markdown-editor-${this.options.useScene ?? 'default'}`);
289+
}
290+
set userPreferredEditor(s) {
291+
window.localStorage.setItem(`markdown-editor-${this.options.useScene ?? 'default'}`, s);
292+
}
257293
}
258294

259295
export function getComboMarkdownEditor(el) {

web_src/js/features/repo-wiki.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ async function initRepoWikiFormEditor() {
4444
renderEasyMDEPreview();
4545

4646
editor = await initComboMarkdownEditor($editorContainer, {
47+
useScene: 'wiki',
48+
// EasyMDE has some problems of height definition, it has inline style height 300px by default, so we also use inline styles to override it.
49+
// And another benefit is that we only need to write the style once for both editors.
50+
// TODO: Move height style to CSS after EasyMDE removal.
51+
editorHeights: {minHeight: '300px', height: 'calc(100vh - 600px)'},
4752
previewMode: 'gfm',
4853
previewWiki: true,
4954
easyMDEOptions: {

web_src/js/utils/dom.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,124 @@ export function onDomReady(cb) {
4949
cb();
5050
}
5151
}
52+
53+
// autosize a textarea to fit content. Based on
54+
// https://github.com/github/textarea-autosize
55+
// ---------------------------------------------------------------------
56+
// Copyright (c) 2018 GitHub, Inc.
57+
//
58+
// Permission is hereby granted, free of charge, to any person obtaining
59+
// a copy of this software and associated documentation files (the
60+
// "Software"), to deal in the Software without restriction, including
61+
// without limitation the rights to use, copy, modify, merge, publish,
62+
// distribute, sublicense, and/or sell copies of the Software, and to
63+
// permit persons to whom the Software is furnished to do so, subject to
64+
// the following conditions:
65+
//
66+
// The above copyright notice and this permission notice shall be
67+
// included in all copies or substantial portions of the Software.
68+
// ---------------------------------------------------------------------
69+
export function autosize(textarea, {viewportMarginBottom = 0} = {}) {
70+
let isUserResized = false;
71+
// lastStyleHeight and initialStyleHeight are CSS values like '100px'
72+
let lastMouseX, lastMouseY, lastStyleHeight, initialStyleHeight;
73+
74+
function onUserResize(event) {
75+
if (isUserResized) return;
76+
if (lastMouseX !== event.clientX || lastMouseY !== event.clientY) {
77+
const newStyleHeight = textarea.style.height;
78+
if (lastStyleHeight && lastStyleHeight !== newStyleHeight) {
79+
isUserResized = true;
80+
}
81+
lastStyleHeight = newStyleHeight;
82+
}
83+
84+
lastMouseX = event.clientX;
85+
lastMouseY = event.clientY;
86+
}
87+
88+
function overflowOffset() {
89+
let offsetTop = 0;
90+
let el = textarea;
91+
92+
while (el !== document.body && el !== null) {
93+
offsetTop += el.offsetTop || 0;
94+
el = el.offsetParent;
95+
}
96+
97+
const top = offsetTop - document.defaultView.scrollY;
98+
const bottom = document.documentElement.clientHeight - (top + textarea.offsetHeight);
99+
return {top, bottom};
100+
}
101+
102+
function resizeToFit() {
103+
if (isUserResized) return;
104+
if (textarea.offsetWidth <= 0 && textarea.offsetHeight <= 0) return;
105+
106+
try {
107+
const {top, bottom} = overflowOffset();
108+
const isOutOfViewport = top < 0 || bottom < 0;
109+
110+
const computedStyle = getComputedStyle(textarea);
111+
const topBorderWidth = parseFloat(computedStyle.borderTopWidth);
112+
const bottomBorderWidth = parseFloat(computedStyle.borderBottomWidth);
113+
const isBorderBox = computedStyle.boxSizing === 'border-box';
114+
const borderAddOn = isBorderBox ? topBorderWidth + bottomBorderWidth : 0;
115+
116+
const adjustedViewportMarginBottom = bottom < viewportMarginBottom ? bottom : viewportMarginBottom;
117+
const curHeight = parseFloat(computedStyle.height);
118+
const maxHeight = curHeight + bottom - adjustedViewportMarginBottom;
119+
120+
textarea.style.height = 'auto';
121+
let newHeight = textarea.scrollHeight + borderAddOn;
122+
123+
if (isOutOfViewport) {
124+
// it is already out of the viewport:
125+
// * if the textarea is expanding: do not resize it
126+
if (newHeight > curHeight) {
127+
newHeight = curHeight;
128+
}
129+
// * if the textarea is shrinking, shrink line by line (just use the
130+
// scrollHeight). do not apply max-height limit, otherwise the page
131+
// flickers and the textarea jumps
132+
} else {
133+
// * if it is in the viewport, apply the max-height limit
134+
newHeight = Math.min(maxHeight, newHeight);
135+
}
136+
137+
textarea.style.height = `${newHeight}px`;
138+
lastStyleHeight = textarea.style.height;
139+
} finally {
140+
// ensure that the textarea is fully scrolled to the end, when the cursor
141+
// is at the end during an input event
142+
if (textarea.selectionStart === textarea.selectionEnd &&
143+
textarea.selectionStart === textarea.value.length) {
144+
textarea.scrollTop = textarea.scrollHeight;
145+
}
146+
}
147+
}
148+
149+
function onFormReset() {
150+
isUserResized = false;
151+
if (initialStyleHeight !== undefined) {
152+
textarea.style.height = initialStyleHeight;
153+
} else {
154+
textarea.style.removeProperty('height');
155+
}
156+
}
157+
158+
textarea.addEventListener('mousemove', onUserResize);
159+
textarea.addEventListener('input', resizeToFit);
160+
textarea.form?.addEventListener('reset', onFormReset);
161+
initialStyleHeight = textarea.style.height ?? undefined;
162+
if (textarea.value) resizeToFit();
163+
164+
return {
165+
resizeToFit,
166+
destroy() {
167+
textarea.removeEventListener('mousemove', onUserResize);
168+
textarea.removeEventListener('input', resizeToFit);
169+
textarea.form?.removeEventListener('reset', onFormReset);
170+
}
171+
};
172+
}

0 commit comments

Comments
 (0)