mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08: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>       </details> --------- Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
		@@ -3,14 +3,17 @@
 | 
			
		||||
{{if not $textareaContent}}{{$textareaContent = .PullRequestTemplate}}{{end}}
 | 
			
		||||
{{if not $textareaContent}}{{$textareaContent = .content}}{{end}}
 | 
			
		||||
 | 
			
		||||
{{template "shared/combomarkdowneditor" (dict
 | 
			
		||||
<div class="field">
 | 
			
		||||
	{{template "shared/combomarkdowneditor" (dict
 | 
			
		||||
		"locale" $.locale
 | 
			
		||||
		"MarkdownPreviewUrl" (print .Repository.Link "/markup")
 | 
			
		||||
		"MarkdownPreviewContext" .RepoLink
 | 
			
		||||
		"TextareaName" "content"
 | 
			
		||||
		"TextareaContent" $textareaContent
 | 
			
		||||
		"TextareaPlaceholder"  ($.locale.Tr "repo.diff.comment.placeholder")
 | 
			
		||||
		"DropzoneParentContainer" "form, .ui.form"
 | 
			
		||||
)}}
 | 
			
		||||
	)}}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
{{if .IsAttachmentEnabled}}
 | 
			
		||||
	<div class="field">
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,15 @@
 | 
			
		||||
  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;
 | 
			
		||||
  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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.repository .comment textarea {
 | 
			
		||||
  max-height: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.repository.new.issue .comment.form .comment .avatar {
 | 
			
		||||
  width: 3em;
 | 
			
		||||
}
 | 
			
		||||
@@ -1068,11 +1064,6 @@
 | 
			
		||||
  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 {
 | 
			
		||||
  margin-top: 10px;
 | 
			
		||||
}
 | 
			
		||||
@@ -1191,15 +1182,6 @@
 | 
			
		||||
  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 {
 | 
			
		||||
  list-style: none;
 | 
			
		||||
  padding-top: 15px;
 | 
			
		||||
@@ -2123,9 +2105,6 @@
 | 
			
		||||
  margin-top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.repository.wiki .form .CodeMirror-scroll {
 | 
			
		||||
  max-height: 85vh;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 767px) {
 | 
			
		||||
  .repository.wiki .dividing.header .stackable.grid .button {
 | 
			
		||||
 
 | 
			
		||||
@@ -154,8 +154,11 @@
 | 
			
		||||
  margin: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.comment-code-cloud .editor-statusbar {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.comment-code-cloud .footer {
 | 
			
		||||
  border-top: 1px solid var(--color-secondary);
 | 
			
		||||
  padding: 10px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -218,15 +221,9 @@ a.blob-excerpt:hover {
 | 
			
		||||
  max-height: calc(100vh - 360px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.review-box-panel .editor-toolbar,
 | 
			
		||||
.review-box-panel .CodeMirror-scroll {
 | 
			
		||||
  width: min(calc(100vw - 2em), 800px);
 | 
			
		||||
  max-width: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.review-box-panel .combo-markdown-editor textarea {
 | 
			
		||||
  width: 730px;
 | 
			
		||||
  max-width: calc(100vw - 70px);
 | 
			
		||||
.review-box-panel .combo-markdown-editor {
 | 
			
		||||
  width: 730px; /* this width matches current EasyMDE's toolbar's width */
 | 
			
		||||
  max-width: calc(100vw - 70px); /* leave enough space on left, and align the page content */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#review-box {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
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 {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 {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
 | 
			
		||||
import {attachRefIssueContextPopup} from '../contextpopup.js';
 | 
			
		||||
@@ -39,31 +39,55 @@ class ComboMarkdownEditor {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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._giteaComboMarkdownEditor = this;
 | 
			
		||||
    this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter)}`;
 | 
			
		||||
    this.textarea.addEventListener('input', (e) => {this.options?.onContentChanged?.(this, e)});
 | 
			
		||||
    this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter++)}`;
 | 
			
		||||
    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.setAttribute('for', this.textarea.id);
 | 
			
		||||
 | 
			
		||||
    elementIdCounter++;
 | 
			
		||||
 | 
			
		||||
    this.switchToEasyMDEButton = this.container.querySelector('.markdown-switch-easymde');
 | 
			
		||||
    this.switchToEasyMDEButton?.addEventListener('click', async (e) => {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      this.userPreferredEditor = 'easymde';
 | 
			
		||||
      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');
 | 
			
		||||
    if (dropzoneParentContainer) {
 | 
			
		||||
      this.dropzone = this.container.closest(this.container.getAttribute('data-dropzone-parent-container'))?.querySelector('.dropzone');
 | 
			
		||||
      initTextareaImagePaste(this.textarea, this.dropzone);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.setupTab();
 | 
			
		||||
    this.prepareEasyMDEToolbarActions();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setupTab() {
 | 
			
		||||
@@ -134,7 +158,10 @@ class ComboMarkdownEditor {
 | 
			
		||||
        title: 'Add Checkbox (checked)',
 | 
			
		||||
      },
 | 
			
		||||
      'gitea-switch-to-textarea': {
 | 
			
		||||
        action: this.switchToTextarea.bind(this),
 | 
			
		||||
        action: () => {
 | 
			
		||||
          this.userPreferredEditor = 'textarea';
 | 
			
		||||
          this.switchToTextarea();
 | 
			
		||||
        },
 | 
			
		||||
        className: 'fa fa-file',
 | 
			
		||||
        title: 'Revert to simple textarea',
 | 
			
		||||
      },
 | 
			
		||||
@@ -169,7 +196,7 @@ class ComboMarkdownEditor {
 | 
			
		||||
    return processed;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async switchToTextarea() {
 | 
			
		||||
  switchToTextarea() {
 | 
			
		||||
    showElem(this.textareaMarkdownToolbar);
 | 
			
		||||
    if (this.easyMDE) {
 | 
			
		||||
      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});
 | 
			
		||||
    initEasyMDEImagePaste(this.easyMDE, this.dropzone);
 | 
			
		||||
    hideElem(this.textareaMarkdownToolbar);
 | 
			
		||||
@@ -236,6 +264,7 @@ class ComboMarkdownEditor {
 | 
			
		||||
    } else {
 | 
			
		||||
      this.textarea.value = v;
 | 
			
		||||
    }
 | 
			
		||||
    this.textareaAutosize.resizeToFit();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  focus() {
 | 
			
		||||
@@ -254,6 +283,13 @@ class ComboMarkdownEditor {
 | 
			
		||||
      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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -44,6 +44,11 @@ async function initRepoWikiFormEditor() {
 | 
			
		||||
  renderEasyMDEPreview();
 | 
			
		||||
 | 
			
		||||
  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',
 | 
			
		||||
    previewWiki: true,
 | 
			
		||||
    easyMDEOptions: {
 | 
			
		||||
 
 | 
			
		||||
@@ -49,3 +49,124 @@ export function onDomReady(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);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user