diff options
author | yp05327 <576951401@qq.com> | 2023-05-09 07:22:52 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-05-08 22:22:52 +0000 |
commit | c4303efc23ea19f16ee826809f43888ee4583ebb (patch) | |
tree | 5ccb95dd7887d45b3e00e3546985d8b57f698a35 /web_src/js/features/comp | |
parent | 9ad5b59cd977e406fe78c6bf38e3698fc4907007 (diff) | |
download | gitea-c4303efc23ea19f16ee826809f43888ee4583ebb.tar.gz gitea-c4303efc23ea19f16ee826809f43888ee4583ebb.zip |
Support markdown editor for issue template (#24400)
Fixes #24398
Task:
- [x] Reusing "textarea" like GitHub seems more friendly to users.
- [x] ^V image pasting and file uploading handling.
<details><summary>screenshots</summary>
![image](https://user-images.githubusercontent.com/18380374/235418877-00090552-ebda-411c-8e39-b47246bc8746.png)
![image](https://user-images.githubusercontent.com/18380374/235419073-dc33cad7-7626-4bce-9161-eb205c7384b5.png)
Display only one markdown editor:
![image](https://user-images.githubusercontent.com/18380374/235419098-ee21386d-2b2d-432e-bdb2-18646cc031e7.png)
Support file upload and ^V image pasting
![image](https://user-images.githubusercontent.com/18380374/235419364-7b390fa4-da56-437d-b55e-3847fbc049e7.png)
</details>
---------
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Diffstat (limited to 'web_src/js/features/comp')
-rw-r--r-- | web_src/js/features/comp/ComboMarkdownEditor.js | 82 | ||||
-rw-r--r-- | web_src/js/features/comp/ImagePaste.js | 8 | ||||
-rw-r--r-- | web_src/js/features/comp/QuickSubmit.js | 4 | ||||
-rw-r--r-- | web_src/js/features/comp/TextExpander.js | 59 |
4 files changed, 88 insertions, 65 deletions
diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js index 90d1bcde5a..103e71daae 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.js +++ b/web_src/js/features/comp/ComboMarkdownEditor.js @@ -5,10 +5,9 @@ import {attachTribute} from '../tribute.js'; import {hideElem, showElem, autosize} from '../../utils/dom.js'; import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js'; import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js'; -import {emojiString} from '../emoji.js'; import {renderPreviewPanelContent} from '../repo-editor.js'; -import {matchEmoji, matchMention} from '../../utils/match.js'; import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js'; +import {initTextExpander} from './TextExpander.js'; let elementIdCounter = 0; @@ -43,14 +42,12 @@ class ComboMarkdownEditor { async init() { this.prepareEasyMDEToolbarActions(); + this.setupContainer(); this.setupTab(); this.setupDropzone(); this.setupTextarea(); - this.setupExpander(); - if (this.userPreferredEditor === 'easymde') { - await this.switchToEasyMDE(); - } + await this.switchToUserPreference(); } applyEditorHeights(el, heights) { @@ -60,6 +57,11 @@ class ComboMarkdownEditor { if (heights.maxHeight) el.style.maxHeight = heights.maxHeight; } + setupContainer() { + initTextExpander(this.container.querySelector('text-expander')); + this.container.addEventListener('ce-editor-content-changed', (e) => this.options?.onContentChanged?.(this, e)); + } + setupTextarea() { this.textarea = this.container.querySelector('.markdown-text-editor'); this.textarea._giteaComboMarkdownEditor = this; @@ -103,64 +105,6 @@ class ComboMarkdownEditor { } } - setupExpander() { - const expander = this.container.querySelector('text-expander'); - expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => { - if (key === ':') { - const matches = matchEmoji(text); - if (!matches.length) return provide({matched: false}); - - const ul = document.createElement('ul'); - ul.classList.add('suggestions'); - for (const name of matches) { - const emoji = emojiString(name); - const li = document.createElement('li'); - li.setAttribute('role', 'option'); - li.setAttribute('data-value', emoji); - li.textContent = `${emoji} ${name}`; - ul.append(li); - } - - provide({matched: true, fragment: ul}); - } else if (key === '@') { - const matches = matchMention(text); - if (!matches.length) return provide({matched: false}); - - const ul = document.createElement('ul'); - ul.classList.add('suggestions'); - for (const {value, name, fullname, avatar} of matches) { - const li = document.createElement('li'); - li.setAttribute('role', 'option'); - li.setAttribute('data-value', `${key}${value}`); - - const img = document.createElement('img'); - img.src = avatar; - li.append(img); - - const nameSpan = document.createElement('span'); - nameSpan.textContent = name; - li.append(nameSpan); - - if (fullname && fullname.toLowerCase() !== name) { - const fullnameSpan = document.createElement('span'); - fullnameSpan.classList.add('fullname'); - fullnameSpan.textContent = fullname; - li.append(fullnameSpan); - } - - ul.append(li); - } - - provide({matched: true, fragment: ul}); - } - }); - expander?.addEventListener('text-expander-value', ({detail}) => { - if (detail?.item) { - detail.value = detail.item.getAttribute('data-value'); - } - }); - } - setupDropzone() { const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container'); if (dropzoneParentContainer) { @@ -224,7 +168,16 @@ class ComboMarkdownEditor { return processed; } + async switchToUserPreference() { + if (this.userPreferredEditor === 'easymde') { + await this.switchToEasyMDE(); + } else { + this.switchToTextarea(); + } + } + switchToTextarea() { + if (!this.easyMDE) return; showElem(this.textareaMarkdownToolbar); if (this.easyMDE) { this.easyMDE.toTextArea(); @@ -233,6 +186,7 @@ class ComboMarkdownEditor { } async switchToEasyMDE() { + if (this.easyMDE) return; // EasyMDE's CSS should be loaded via webpack config, otherwise our own styles can not overwrite the default styles. const {default: EasyMDE} = await import(/* webpackChunkName: "easymde" */'easymde'); const easyMDEOpt = { diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js index 9145b24062..dc335495a3 100644 --- a/web_src/js/features/comp/ImagePaste.js +++ b/web_src/js/features/comp/ImagePaste.js @@ -25,6 +25,10 @@ function clipboardPastedImages(e) { return files; } +function triggerEditorContentChanged(target) { + target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true})); +} + class TextareaEditor { constructor(editor) { this.editor = editor; @@ -38,6 +42,7 @@ class TextareaEditor { editor.selectionStart = startPos; editor.selectionEnd = startPos + value.length; editor.focus(); + triggerEditorContentChanged(editor); } replacePlaceholder(oldVal, newVal) { @@ -54,6 +59,7 @@ class TextareaEditor { } editor.selectionStart = editor.selectionEnd; editor.focus(); + triggerEditorContentChanged(editor); } } @@ -70,6 +76,7 @@ class CodeMirrorEditor { endPoint.ch = startPoint.ch + value.length; editor.setSelection(startPoint, endPoint); editor.focus(); + triggerEditorContentChanged(editor.getTextArea()); } replacePlaceholder(oldVal, newVal) { @@ -84,6 +91,7 @@ class CodeMirrorEditor { endPoint.ch += newVal.length; editor.setSelection(endPoint, endPoint); editor.focus(); + triggerEditorContentChanged(editor.getTextArea()); } } diff --git a/web_src/js/features/comp/QuickSubmit.js b/web_src/js/features/comp/QuickSubmit.js index 43424a949f..d598a59655 100644 --- a/web_src/js/features/comp/QuickSubmit.js +++ b/web_src/js/features/comp/QuickSubmit.js @@ -6,7 +6,9 @@ export function handleGlobalEnterQuickSubmit(target) { if ($form.length) { // here use the event to trigger the submit event (instead of calling `submit()` method directly) // otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog - $form.trigger('submit'); + if ($form[0].checkValidity()) { + $form.trigger('submit'); + } } else { // if no form, then the editor is for an AJAX request, dispatch an event to the target, let the target's event handler to do the AJAX request. // the 'ce-' prefix means this is a CustomEvent diff --git a/web_src/js/features/comp/TextExpander.js b/web_src/js/features/comp/TextExpander.js new file mode 100644 index 0000000000..e2840610df --- /dev/null +++ b/web_src/js/features/comp/TextExpander.js @@ -0,0 +1,59 @@ +import {matchEmoji, matchMention} from '../../utils/match.js'; +import {emojiString} from '../emoji.js'; + +export function initTextExpander(expander) { + expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => { + if (key === ':') { + const matches = matchEmoji(text); + if (!matches.length) return provide({matched: false}); + + const ul = document.createElement('ul'); + ul.classList.add('suggestions'); + for (const name of matches) { + const emoji = emojiString(name); + const li = document.createElement('li'); + li.setAttribute('role', 'option'); + li.setAttribute('data-value', emoji); + li.textContent = `${emoji} ${name}`; + ul.append(li); + } + + provide({matched: true, fragment: ul}); + } else if (key === '@') { + const matches = matchMention(text); + if (!matches.length) return provide({matched: false}); + + const ul = document.createElement('ul'); + ul.classList.add('suggestions'); + for (const {value, name, fullname, avatar} of matches) { + const li = document.createElement('li'); + li.setAttribute('role', 'option'); + li.setAttribute('data-value', `${key}${value}`); + + const img = document.createElement('img'); + img.src = avatar; + li.append(img); + + const nameSpan = document.createElement('span'); + nameSpan.textContent = name; + li.append(nameSpan); + + if (fullname && fullname.toLowerCase() !== name) { + const fullnameSpan = document.createElement('span'); + fullnameSpan.classList.add('fullname'); + fullnameSpan.textContent = fullname; + li.append(fullnameSpan); + } + + ul.append(li); + } + + provide({matched: true, fragment: ul}); + } + }); + expander?.addEventListener('text-expander-value', ({detail}) => { + if (detail?.item) { + detail.value = detail.item.getAttribute('data-value'); + } + }); +} |